Merge branch 'next' into edge-db

This commit is contained in:
Ikechukwu Eze 2022-07-31 19:54:30 +02:00 committed by GitHub
commit 801b9c1483
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 3998 additions and 2151 deletions

View File

@ -15,6 +15,7 @@
"settings": {}, "settings": {},
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"svelte.svelte-vscode", "svelte.svelte-vscode",
"ardenivanov.svelte-intellisense", "ardenivanov.svelte-intellisense",

View File

@ -3,6 +3,6 @@ contact_links:
- name: 🤔 Questions and Help - name: 🤔 Questions and Help
url: https://discord.com/invite/6rDM4fkymF url: https://discord.com/invite/6rDM4fkymF
about: Reach out to us on discord or our github discussions page. about: Reach out to us on discord or our github discussions page.
- name: 🙋‍♂️ service request - name: 🙋‍♂️ Service request
url: https://feedback.coolify.io/ url: https://feedback.coolify.io/
about: want to request a new service? for e.g wordpress, hasura, appwrite etc... about: want to request a new service? for e.g wordpress, hasura, appwrite etc...

View File

@ -9,3 +9,5 @@ tasks:
ports: ports:
- port: 3001 - port: 3001
visibility: public visibility: public
- port: 3000
visibility: public

View File

@ -1,4 +1,4 @@
FROM node:18-alpine as build FROM node:18-alpine3.16 as build
WORKDIR /app WORKDIR /app
RUN apk add --no-cache curl RUN apk add --no-cache curl
@ -9,7 +9,7 @@ RUN pnpm install
RUN pnpm build RUN pnpm build
# Production build # Production build
FROM node:18-alpine FROM node:18-alpine3.16
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -27,8 +27,10 @@ RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
RUN mkdir -p ~/.docker/cli-plugins/ RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.3.4 -o ~/.docker/cli-plugins/docker-compose # https://github.com/docker/compose/releases
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.7.0 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
COPY --from=build /app/apps/api/build/ . COPY --from=build /app/apps/api/build/ .

103
README.md
View File

@ -1,6 +1,32 @@
# Coolify # Coolify
An open-source & self-hostable Heroku / Netlify alternative. An open-source & self-hostable Heroku / Netlify alternative
(ARM support is in beta).
## Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
### Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
### Organizations
Support this project with your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/1/website"><img src="https://opencollective.com/coollabsio/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/2/website"><img src="https://opencollective.com/coollabsio/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/3/website"><img src="https://opencollective.com/coollabsio/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/4/website"><img src="https://opencollective.com/coollabsio/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/5/website"><img src="https://opencollective.com/coollabsio/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/6/website"><img src="https://opencollective.com/coollabsio/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
---
## Live Demo ## Live Demo
@ -12,6 +38,8 @@ https://demo.coolify.io/
If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
---
## How to install ## How to install
Installation is automated with the following command: Installation is automated with the following command:
@ -28,60 +56,65 @@ wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./in
For more details goto the [docs](https://docs.coollabs.io/coolify/installation). For more details goto the [docs](https://docs.coollabs.io/coolify/installation).
## Features ---
ARM support is in beta! ## Features
### Git Sources ### Git Sources
You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self-hosted versions are also supported.) You can use the following Git Sources to be auto-deployed to your Coolify instance! (Self-hosted versions are also supported.)
- Github <a href="https://github.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/github.com"></a>
- GitLab <a href="https://gitlab.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/gitlab.com"></a>
- Bitbucket (WIP)
### Destinations ### Destinations
You can deploy your applications to the following destinations: You can deploy your applications to the following destinations:
- Local Docker Engine - Local Docker Engine
- Remote Docker Engine (WIP) - Remote Docker Engine
- Kubernetes (WIP)
### Applications ### Applications
These are the predefined build packs, but with the Docker build pack, you can host anything that is hostable with a single Dockerfile. Predefined build packs to cover the basic needs to deploy applications.
- Static sites If you have an advanced use case, you can use the Docker build pack that allows you to deploy your application based on your custom Dockerfile.
- NodeJS
- VueJS <a href="https://html5.org/">
- NuxtJS <svg style="width:40px;height:40px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" ><g clip-path="url(#HTML5_Clip0_4)" ><path d="M30.216 0L27.6454 28.7967L16.0907 32L4.56783 28.8012L2 0H30.216Z" fill="#E44D26" /><path d="M16.108 29.5515L25.4447 26.963L27.6415 2.35497H16.108V29.5515Z" fill="#F16529" /><path d="M11.1109 9.4197H16.108V5.88731H7.25053L7.33509 6.83499L8.20327 16.5692H16.108V13.0369H11.4338L11.1109 9.4197Z" fill="#EBEBEB" /><path d="M11.907 18.3354H8.36111L8.856 23.8818L16.0917 25.8904L16.108 25.8859V22.2108L16.0925 22.2149L12.1585 21.1527L11.907 18.3354Z" fill="#EBEBEB" /><path d="M16.0958 16.5692H20.4455L20.0354 21.1504L16.0958 22.2138V25.8887L23.3373 23.8817L23.3904 23.285L24.2205 13.9855L24.3067 13.0369H16.0958V16.5692Z" fill="white" /><path d="M16.0958 9.41105V9.41969H24.6281L24.6989 8.62572L24.8599 6.83499L24.9444 5.88731H16.0958V9.41105Z" fill="white" /></g><defs><clipPath id="HTML5_Clip0_4"><rect width="32" height="32" fill="white" /></clipPath></defs></svg></a>
- NextJS <a href="https://nodejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nodejs.org"></a>
- React/Preact <a href="https://vuejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/vuejs.org"></a>
- Gatsby <a href="https://nuxtjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nuxtjs.org"></a>
- Svelte <a href="https://nextjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nextjs.org"></a>
- PHP <a href="https://reactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/reactjs.org"></a>
- Laravel <a href="https://preactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/preactjs.org"></a>
- Rust <a href="https://gatsbyjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/gatsbyjs.org"></a>
- Docker <a href="https://svelte.dev"><img style="width:40px;height:40px" src="https://icon.horse/icon/svelte.dev"></a>
- Python <a href="https://php.net"><img style="width:40px;height:40px" src="https://icon.horse/icon/php.net"></a>
- Deno <a href="https://laravel.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/laravel.com"></a>
<a href="https://python.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/python.org"></a>
<a href="https://deno.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/deno.com"></a>
<a href="https://docker.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/docker.com"></a>
If you have a new build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
### Databases ### Databases
One-click database is ready to be used internally or shared over the internet: One-click database is ready to be used internally or shared over the internet:
- MongoDB <a href="https://mongodb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mongodb.org"></a>
- MariaDB <a href="https://mariadb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mariadb.org"></a>
- MySQL <a href="https://mysql.com"><svg style="width:40px;height:40px" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 25.6 25.6" ><path d="M179.076 94.886c-3.568-.1-6.336.268-8.656 1.25-.668.27-1.74.27-1.828 1.116.357.355.4.936.713 1.428.535.893 1.473 2.096 2.32 2.72l2.855 2.053c1.74 1.07 3.703 1.695 5.398 2.766.982.625 1.963 1.428 2.945 2.098.5.357.803.938 1.428 1.16v-.135c-.312-.4-.402-.98-.713-1.428l-1.34-1.293c-1.293-1.74-2.9-3.258-4.64-4.506-1.428-.982-4.55-2.32-5.13-3.97l-.088-.1c.98-.1 2.14-.447 3.078-.715 1.518-.4 2.9-.312 4.46-.713l2.143-.625v-.4c-.803-.803-1.383-1.874-2.23-2.632-2.275-1.963-4.775-3.882-7.363-5.488-1.383-.892-3.168-1.473-4.64-2.23-.537-.268-1.428-.402-1.74-.848-.805-.98-1.25-2.275-1.83-3.436l-3.658-7.763c-.803-1.74-1.295-3.48-2.275-5.086-4.596-7.585-9.594-12.18-17.268-16.687-1.65-.937-3.613-1.34-5.7-1.83l-3.346-.18c-.715-.312-1.428-1.16-2.053-1.562-2.543-1.606-9.102-5.086-10.977-.5-1.205 2.9 1.785 5.755 2.8 7.228.76 1.026 1.74 2.186 2.277 3.346.3.758.4 1.562.713 2.365.713 1.963 1.383 4.15 2.32 5.98.5.937 1.025 1.92 1.65 2.767.357.5.982.714 1.115 1.517-.625.893-.668 2.23-1.025 3.347-1.607 5.042-.982 11.288 1.293 15 .715 1.115 2.4 3.57 4.686 2.632 2.008-.803 1.56-3.346 2.14-5.577.135-.535.045-.892.312-1.25v.1l1.83 3.703c1.383 2.186 3.793 4.462 5.8 5.98 1.07.803 1.918 2.187 3.256 2.677v-.135h-.088c-.268-.4-.67-.58-1.027-.892-.803-.803-1.695-1.785-2.32-2.677-1.873-2.498-3.523-5.265-4.996-8.12-.715-1.383-1.34-2.9-1.918-4.283-.27-.536-.27-1.34-.715-1.606-.67.98-1.65 1.83-2.143 3.034-.848 1.918-.936 4.283-1.248 6.737-.18.045-.1 0-.18.1-1.426-.356-1.918-1.83-2.453-3.078-1.338-3.168-1.562-8.254-.402-11.913.312-.937 1.652-3.882 1.117-4.774-.27-.848-1.16-1.338-1.652-2.008-.58-.848-1.203-1.918-1.605-2.855-1.07-2.5-1.605-5.265-2.766-7.764-.537-1.16-1.473-2.365-2.232-3.435-.848-1.205-1.783-2.053-2.453-3.48-.223-.5-.535-1.294-.178-1.83.088-.357.268-.5.623-.58.58-.5 2.232.134 2.812.4 1.65.67 3.033 1.294 4.416 2.23.625.446 1.295 1.294 2.098 1.518h.938c1.428.312 3.033.1 4.37.5 2.365.76 4.506 1.874 6.426 3.08 5.844 3.703 10.664 8.968 13.92 15.26.535 1.026.758 1.963 1.25 3.034.938 2.187 2.098 4.417 3.033 6.56.938 2.097 1.83 4.24 3.168 5.98.67.937 3.346 1.427 4.55 1.918.893.4 2.275.76 3.08 1.25 1.516.937 3.033 2.008 4.46 3.034.713.534 2.945 1.65 3.078 2.54zm-45.5-38.772a7.09 7.09 0 0 0-1.828.223v.1h.088c.357.714.982 1.205 1.428 1.83l1.027 2.142.088-.1c.625-.446.938-1.16.938-2.23-.268-.312-.312-.625-.535-.937-.268-.446-.848-.67-1.206-1.026z" transform="matrix(.390229 0 0 .38781 -46.300037 -16.856717)" fill-rule="evenodd" fill="#00678c" /></svg></a>
- PostgreSQL <a href="https://postgresql.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/postgresql.org"></a>
- CouchDB <a href="https://couchdb.apache.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/couchdb.apache.org"></a>
- Redis <a href="https://redis.io"><svg style="width:40px;height:40px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ><defs ><path id="a" d="m45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" /><path id="b" d="m45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z" /></defs ><g transform="matrix(.848327 0 0 .848327 -7.883573 -9.449691)" ><use fill="#a41e11" xlink:href="#a" /><path d="m45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14s12.35 4.852 14.31 5.582 2.037 1.31.024 2.36z" fill="#d82c20" /><use fill="#a41e11" xlink:href="#a" y="-6.218" /><use fill="#d82c20" xlink:href="#b" /><path d="m45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.815s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" fill="#a41e11" /><use fill="#d82c20" xlink:href="#b" y="-6.449" /><g fill="#fff" ><path d="m29.096 20.712-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zm-6.296 3.912 8.74-1.342-2.64 3.872z" /><ellipse cx="20.444" cy="21.402" rx="4.672" ry="1.811" /></g ><path d="m42.132 21.138-5.17 2.042-.004-4.087z" fill="#7a0c00" /><path d="m36.963 23.18-.56.22-5.166-2.042 5.723-2.264z" fill="#ad2115" /></g ></svg ></a>
### One-click services If you have a new database you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
You can host cool open-source services as well:
### Services
You quickly need to host a self-hostable, open-source service? You can do it with a few clicks!
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) - [WordPress](https://docs.coollabs.io/coolify/services/wordpress)
- [Ghost](https://ghost.org) - [Ghost](https://ghost.org)
- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) - [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics)
@ -97,6 +130,9 @@ You can host cool open-source services as well:
- [Fider](https://fider.io) - [Fider](https://fider.io)
- [Hasura](https://hasura.io) - [Hasura](https://hasura.io)
If you have a new service you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
## Migration from v1 ## Migration from v1
A fresh installation is necessary. v2 and v3 are not compatible with v1. A fresh installation is necessary. v2 and v3 are not compatible with v1.
@ -108,9 +144,6 @@ A fresh installation is necessary. v2 and v3 are not compatible with v1.
- Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.gg/xhBCC7eGKw) - Discord: [Invitation](https://discord.gg/xhBCC7eGKw)
## Contribute
See [our contribution guide](./CONTRIBUTING.md).
## License ## License

6
TODO.md Normal file
View File

@ -0,0 +1,6 @@
- RDE Application DNS check not working
- Check DNS configurations for app/service/coolify with RDE and local engines
# Low
- Create previews model in Coolify DB

View File

@ -16,11 +16,11 @@
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.1.0", "@fastify/autoload": "5.1.0",
"@fastify/cookie": "7.1.0", "@fastify/cookie": "7.3.1",
"@fastify/cors": "8.0.0", "@fastify/cors": "8.0.0",
"@fastify/env": "4.0.0", "@fastify/env": "4.0.0",
"@fastify/jwt": "6.3.1", "@fastify/jwt": "6.3.1",
"@fastify/static": "6.4.0", "@fastify/static": "6.4.1",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.15.2", "@prisma/client": "3.15.2",
"axios": "0.27.2", "axios": "0.27.2",
@ -29,30 +29,33 @@
"cabin": "9.1.2", "cabin": "9.1.2",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.3", "dayjs": "1.11.4",
"dockerode": "3.3.2", "dockerode": "3.3.2",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"fastify": "4.2.1", "fastify": "4.3.0",
"fastify-plugin": "4.0.0", "fastify-plugin": "4.0.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2", "get-port": "6.1.2",
"got": "12.1.0", "got": "12.2.0",
"is-ip": "4.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",
"p-queue": "7.2.0", "p-queue": "7.2.0",
"public-ip": "6.0.1",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.0.4", "@types/node": "18.6.1",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.30.6", "@typescript-eslint/eslint-plugin": "5.31.0",
"@typescript-eslint/parser": "5.30.6", "@typescript-eslint/parser": "5.31.0",
"esbuild": "0.14.49", "esbuild": "0.14.50",
"eslint": "8.19.0", "eslint": "8.20.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19", "nodemon": "2.0.19",

View File

@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Moodle" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"defaultUsername" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"defaultEmail" TEXT NOT NULL,
"mariadbUser" TEXT NOT NULL,
"mariadbPassword" TEXT NOT NULL,
"mariadbRootUser" TEXT NOT NULL,
"mariadbRootUserPassword" TEXT NOT NULL,
"mariadbDatabase" TEXT NOT NULL,
"mariadbPublicPort" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Moodle_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Moodle_serviceId_key" ON "Moodle"("serviceId");

View File

@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,33 @@
-- CreateTable
CREATE TABLE "SshKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_DestinationDocker" (
"id" TEXT NOT NULL PRIMARY KEY,
"network" TEXT NOT NULL,
"name" TEXT NOT NULL,
"engine" TEXT,
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
"remoteIpAddress" TEXT,
"remoteUser" TEXT,
"remotePort" INTEGER,
"remoteVerified" BOOLEAN NOT NULL DEFAULT false,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"sshKeyId" TEXT,
CONSTRAINT "DestinationDocker_sshKeyId_fkey" FOREIGN KEY ("sshKeyId") REFERENCES "SshKey" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "ipv4" TEXT;
ALTER TABLE "Setting" ADD COLUMN "ipv6" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "arch" TEXT;

View File

@ -0,0 +1,16 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_SshKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"teamId" TEXT,
CONSTRAINT "SshKey_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_SshKey" ("createdAt", "id", "name", "privateKey", "updatedAt") SELECT "createdAt", "id", "name", "privateKey", "updatedAt" FROM "SshKey";
DROP TABLE "SshKey";
ALTER TABLE "new_SshKey" RENAME TO "SshKey";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -23,6 +23,9 @@ model Setting {
isTraefikUsed Boolean @default(true) isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
ipv4 String?
ipv6 String?
arch String?
} }
model User { model User {
@ -30,39 +33,40 @@ model User {
email String @unique email String @unique
type String type String
password String? password String?
teams Team[]
permission Permission[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
permission Permission[]
teams Team[]
} }
model Permission { model Permission {
id String @id @default(cuid()) id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String userId String
team Team @relation(fields: [teamId], references: [id])
teamId String teamId String
permission String permission String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
} }
model Team { model Team {
id String @id @default(cuid()) id String @id @default(cuid())
users User[]
name String? name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
databaseId String?
serviceId String?
permissions Permission[]
sshKey SshKey[]
applications Application[] applications Application[]
database Database[]
destinationDocker DestinationDocker[]
gitSources GitSource[] gitSources GitSource[]
gitHubApps GithubApp[] gitHubApps GithubApp[]
gitLabApps GitlabApp[] gitLabApps GitlabApp[]
destinationDocker DestinationDocker[] service Service[]
permissions Permission[] users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
database Database[] @relation(references: [id])
databaseId String?
service Service[] @relation(references: [id])
serviceId String?
} }
model TeamInvitation { model TeamInvitation {
@ -101,21 +105,20 @@ model Application {
denoOptions String? denoOptions String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings?
teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
gitSourceId String? gitSourceId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[]
persistentStorage ApplicationPersistentStorage[]
baseImage String? baseImage String?
baseBuildImage String? baseBuildImage String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ApplicationPersistentStorage[]
settings ApplicationSettings?
secrets Secret[]
teams Team[]
} }
model ApplicationSettings { model ApplicationSettings {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique applicationId String @unique
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
debug Boolean @default(false) debug Boolean @default(false)
@ -123,26 +126,27 @@ model ApplicationSettings {
autodeploy Boolean @default(true) autodeploy Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
} }
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String applicationId String
path String path String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
@@unique([applicationId, path]) @@unique([applicationId, path])
} }
model ServicePersistentStorage { model ServicePersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
service Service @relation(fields: [serviceId], references: [id])
serviceId String serviceId String
path String path String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@unique([serviceId, path]) @@unique([serviceId, path])
} }
@ -155,8 +159,8 @@ model Secret {
isBuildSecret Boolean @default(false) isBuildSecret Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
applicationId String applicationId String
application Application @relation(fields: [applicationId], references: [id])
@@unique([name, applicationId, isPRMRSecret]) @@unique([name, applicationId, isPRMRSecret])
} }
@ -167,8 +171,8 @@ model ServiceSecret {
value String value String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
serviceId String serviceId String
service Service @relation(fields: [serviceId], references: [id])
@@unique([name, serviceId]) @@unique([name, serviceId])
} }
@ -200,21 +204,37 @@ model DestinationDocker {
id String @id @default(cuid()) id String @id @default(cuid())
network String @unique network String @unique
name String name String
engine String engine String?
remoteEngine Boolean @default(false) remoteEngine Boolean @default(false)
remoteIpAddress String?
remoteUser String?
remotePort Int?
remoteVerified Boolean @default(false)
isCoolifyProxyUsed Boolean? @default(false) isCoolifyProxyUsed Boolean? @default(false)
teams Team[]
application Application[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
sshKeyId String?
sshKey SshKey? @relation(fields: [sshKeyId], references: [id])
application Application[]
database Database[] database Database[]
service Service[] service Service[]
teams Team[]
}
model SshKey {
id String @id @default(cuid())
name String
privateKey String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamId String?
team Team? @relation(fields: [teamId], references: [id])
destinationDocker DestinationDocker[]
} }
model GitSource { model GitSource {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
teams Team[]
type String? type String?
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
@ -223,16 +243,16 @@ model GitSource {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
githubAppId String? @unique githubAppId String? @unique
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[]
gitlabAppId String? @unique gitlabAppId String? @unique
gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id]) gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id])
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[]
teams Team[]
} }
model GithubApp { model GithubApp {
id String @id @default(cuid()) id String @id @default(cuid())
name String? @unique name String? @unique
teams Team[]
appId Int? appId Int?
installationId Int? installationId Int?
clientId String? clientId String?
@ -242,13 +262,13 @@ model GithubApp {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
gitSource GitSource? gitSource GitSource?
teams Team[]
} }
model GitlabApp { model GitlabApp {
id String @id @default(cuid()) id String @id @default(cuid())
oauthId Int @unique oauthId Int @unique
groupName String? @unique groupName String? @unique
teams Team[]
deployKeyId Int? deployKeyId Int?
privateSshKey String? privateSshKey String?
publicSshKey String? publicSshKey String?
@ -258,6 +278,7 @@ model GitlabApp {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
gitSource GitSource? gitSource GitSource?
teams Team[]
} }
model Database { model Database {
@ -271,22 +292,22 @@ model Database {
dbUserPassword String? dbUserPassword String?
rootUser String? rootUser String?
rootUserPassword String? rootUserPassword String?
settings DatabaseSettings?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
destinationDockerId String? destinationDockerId String?
teams Team[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
settings DatabaseSettings?
teams Team[]
} }
model DatabaseSettings { model DatabaseSettings {
id String @id @default(cuid()) id String @id @default(cuid())
database Database @relation(fields: [databaseId], references: [id])
databaseId String @unique databaseId String @unique
isPublic Boolean @default(false) isPublic Boolean @default(false)
appendOnly Boolean @default(true) appendOnly Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
database Database @relation(fields: [databaseId], references: [id])
} }
model Service { model Service {
@ -297,23 +318,23 @@ model Service {
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
teams Team[]
destinationDockerId String? destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
plausibleAnalytics PlausibleAnalytics? destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
fider Fider?
ghost Ghost?
hasura Hasura?
meiliSearch MeiliSearch?
minio Minio? minio Minio?
moodle Moodle?
plausibleAnalytics PlausibleAnalytics?
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
umami Umami?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
ghost Ghost? teams Team[]
serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
umami Umami?
hasura Hasura?
fider Fider?
moodle Moodle?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -328,9 +349,9 @@ model PlausibleAnalytics {
secretKeyBase String? secretKeyBase String?
scriptName String @default("plausible.js") scriptName String @default("plausible.js")
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Minio { model Minio {
@ -340,18 +361,18 @@ model Minio {
publicPort Int? publicPort Int?
apiFqdn String? apiFqdn String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Vscodeserver { model Vscodeserver {
id String @id @default(cuid()) id String @id @default(cuid())
password String password String
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Wordpress { model Wordpress {
@ -374,9 +395,9 @@ model Wordpress {
ftpHostKey String? ftpHostKey String?
ftpHostKeyPrivate String? ftpHostKeyPrivate String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Ghost { model Ghost {
@ -390,18 +411,18 @@ model Ghost {
mariadbDatabase String? mariadbDatabase String?
mariadbPublicPort Int? mariadbPublicPort Int?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model MeiliSearch { model MeiliSearch {
id String @id @default(cuid()) id String @id @default(cuid())
masterKey String masterKey String
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Umami { model Umami {
@ -413,9 +434,9 @@ model Umami {
postgresqlPublicPort Int? postgresqlPublicPort Int?
umamiAdminPassword String umamiAdminPassword String
hashSalt String hashSalt String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Hasura { model Hasura {
@ -426,9 +447,9 @@ model Hasura {
postgresqlDatabase String postgresqlDatabase String
postgresqlPublicPort Int? postgresqlPublicPort Int?
graphQLAdminPassword String graphQLAdminPassword String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Fider { model Fider {
@ -448,9 +469,9 @@ model Fider {
emailSmtpUser String? emailSmtpUser String?
emailSmtpPassword String? emailSmtpPassword String?
emailSmtpEnableStartTls Boolean @default(false) emailSmtpEnableStartTls Boolean @default(false)
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }
model Moodle { model Moodle {
@ -465,7 +486,7 @@ model Moodle {
mariadbRootUserPassword String mariadbRootUserPassword String
mariadbDatabase String mariadbDatabase String
mariadbPublicPort Int? mariadbPublicPort Int?
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
} }

View File

@ -24,7 +24,8 @@ async function main() {
data: { data: {
isRegistrationEnabled: true, isRegistrationEnabled: true,
proxyPassword: encrypt(generatePassword()), proxyPassword: encrypt(generatePassword()),
proxyUser: cuid() proxyUser: cuid(),
arch: process.arch
} }
}); });
} else { } else {

View File

@ -5,7 +5,7 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, isDev, prisma } from './lib/common'; import { asyncExecShell, isDev, listSettings, prisma } from './lib/common';
import { scheduler } from './lib/scheduler'; import { scheduler } from './lib/scheduler';
declare module 'fastify' { declare module 'fastify' {
@ -101,10 +101,10 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
process.exit(1); process.exit(1);
} }
console.log(`Coolify's API is listening on ${host}:${port}`); console.log(`Coolify's API is listening on ${host}:${port}`);
await initServer() await initServer();
await scheduler.start('deployApplication'); await scheduler.start('deployApplication');
await scheduler.start('cleanupStorage'); await scheduler.start('cleanupStorage');
await scheduler.start('checkProxies') await scheduler.start('checkProxies');
// Check if no build is running // Check if no build is running
@ -130,12 +130,37 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
if (!scheduler.workers.has('deployApplication')) await scheduler.start('deployApplication'); if (!scheduler.workers.has('deployApplication')) await scheduler.start('deployApplication');
} }
}); });
await getArch();
await getIPAddress();
}); });
async function getIPAddress() {
const { publicIpv4, publicIpv6 } = await import('public-ip')
try {
const settings = await listSettings();
if (!settings.ipv4) {
const ipv4 = await publicIpv4({ timeout: 2000 })
await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } })
}
if (!settings.ipv6) {
const ipv6 = await publicIpv6({ timeout: 2000 })
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } })
}
} catch (error) { }
}
async function initServer() { async function initServer() {
try { try {
await asyncExecShell(`docker network create --attachable coolify`); await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) { } } catch (error) { }
} }
async function getArch() {
try {
const settings = await prisma.setting.findFirst({})
if (settings && !settings.arch) {
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } })
}
} catch (error) { }
}

View File

@ -1,24 +1,24 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, asyncExecShell } from '../lib/common'; import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd } from '../lib/common';
import { checkContainer, getEngine } from '../lib/docker'; import { checkContainer } from '../lib/docker';
(async () => { (async () => {
if (parentPort) { if (parentPort) {
// Coolify Proxy // Coolify Proxy local
const engine = '/var/run/docker.sock'; const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({ const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' } where: { engine, network: 'coolify' }
}); });
if (localDocker && localDocker.isCoolifyProxyUsed) { if (localDocker && localDocker.isCoolifyProxyUsed) {
// Remove HAProxy // Remove HAProxy
const found = await checkContainer(engine, 'coolify-haproxy'); const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' });
const host = getEngine(engine);
if (found) { if (found) {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy` dockerId: localDocker.id,
); command: `docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
})
} }
await startTraefikProxy(engine); await startTraefikProxy(localDocker.id);
} }
@ -32,12 +32,14 @@ import { checkContainer, getEngine } from '../lib/docker';
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database); const { privatePort } = generateDatabaseConfiguration(database);
// Remove HAProxy // Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${publicPort}`); const found = await checkContainer({
const host = getEngine(engine); dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
});
if (found) { if (found) {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}` dockerId: localDocker.id,
); command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
})
} }
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
@ -52,12 +54,12 @@ import { checkContainer, getEngine } from '../lib/docker';
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy // Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${ftpPublicPort}`); const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` });
const host = getEngine(engine);
if (found) { if (found) {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort} ` dockerId: localDocker.id,
); command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}`
})
} }
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp'); await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
} }
@ -73,12 +75,12 @@ import { checkContainer, getEngine } from '../lib/docker';
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy // Remove HAProxy
const found = await checkContainer(engine, `${id}-${publicPort}`); const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
const host = getEngine(engine);
if (found) { if (found) {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST="${host}" docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort}` dockerId: localDocker.id,
); command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
})
} }
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
} }

View File

@ -1,20 +1,20 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import { asyncExecShell, cleanupDockerStorage, isDev, prisma, version } from '../lib/common'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, version } from '../lib/common';
import { getEngine } from '../lib/docker';
(async () => { (async () => {
if (parentPort) { if (parentPort) {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))]; let enginesDone = new Set()
for (const engine of engines) { for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false; let lowDiskSpace = false;
const host = getEngine(engine);
try { try {
let stdout = null let stdout = null
if (!isDev) { if (!isDev) {
const output = await asyncExecShell( const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
`DOCKER_HOST=${host} docker exec coolify sh -c 'df -kPT /'`
);
stdout = output.stdout; stdout = output.stdout;
} else { } else {
const output = await asyncExecShell( const output = await asyncExecShell(
@ -53,7 +53,7 @@ import { getEngine } from '../lib/docker';
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
await cleanupDockerStorage(host, lowDiskSpace, false) await cleanupDockerStorage(destination.id, lowDiskSpace, false)
} }
await prisma.$disconnect(); await prisma.$disconnect();
} else process.exit(0); } else process.exit(0);

View File

@ -4,8 +4,7 @@ import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
import { asyncExecShell, createDirectories, decrypt, getDomain, prisma } from '../lib/common'; import { createDirectories, decrypt, executeDockerCmd, getDomain, prisma } from '../lib/common';
import { dockerInstance, getEngine } from '../lib/docker';
import * as importers from '../lib/importers'; import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks'; import * as buildpacks from '../lib/buildPacks';
@ -104,9 +103,6 @@ import * as buildpacks from '../lib/buildPacks';
destinationType = 'docker'; destinationType = 'docker';
} }
if (destinationType === 'docker') { if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine);
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
const { workdir, repodir } = await createDirectories({ repository, buildId }); const { workdir, repodir } = await createDirectories({ repository, buildId });
const configuration = await setDefaultConfiguration(message); const configuration = await setDefaultConfiguration(message);
@ -185,18 +181,23 @@ import * as buildpacks from '../lib/buildPacks';
} else { } else {
deployNeeded = true; deployNeeded = true;
} }
const image = await docker.engine.getImage(`${applicationId}:${tag}`);
let imageFound = false; let imageFound = false;
try { try {
await image.inspect(); await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}`
})
imageFound = true; imageFound = true;
} catch (error) { } catch (error) {
// //
} }
if (!imageFound || deployNeeded) { // if (!imageFound || deployNeeded) {
if (true) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
dockerId: destinationDocker.id,
buildId, buildId,
applicationId, applicationId,
domain, domain,
@ -212,7 +213,6 @@ import * as buildpacks from '../lib/buildPacks';
commit, commit,
tag, tag,
workdir, workdir,
docker,
port: exposePort ? `${exposePort}:${port}` : port, port: exposePort ? `${exposePort}:${port}` : port,
installCommand, installCommand,
buildCommand, buildCommand,
@ -238,8 +238,8 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId }); await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${imageId}`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${imageId}`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
} catch (error) { } catch (error) {
// //
} }
@ -299,7 +299,7 @@ import * as buildpacks from '../lib/buildPacks';
container_name: imageId, container_name: imageId,
volumes, volumes,
env_file: envFound ? [`${workdir}/.env`] : [], env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network], networks: [destinationDocker.network],
labels, labels,
depends_on: [], depends_on: [],
restart: 'always', restart: 'always',
@ -318,16 +318,14 @@ import * as buildpacks from '../lib/buildPacks';
} }
}, },
networks: { networks: {
[docker.network]: { [destinationDocker.network]: {
external: true external: true
} }
}, },
volumes: Object.assign({}, ...composeVolumes) volumes: Object.assign({}, ...composeVolumes)
}; };
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await asyncExecShell( await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
);
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });

View File

@ -1,7 +1,7 @@
import { asyncExecShell, base64Encode, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { scheduler } from "../scheduler";
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { day } from "../dayjs"; import { day } from "../dayjs";
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [ const nodeBased = [
'react', 'react',
@ -511,8 +511,8 @@ export async function buildImage({
applicationId, applicationId,
tag, tag,
workdir, workdir,
docker,
buildId, buildId,
dockerId,
isCache = false, isCache = false,
debug = false, debug = false,
dockerFileLocation = '/Dockerfile' dockerFileLocation = '/Dockerfile'
@ -522,6 +522,9 @@ export async function buildImage({
} else { } else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (debug) {
await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment. You will see the full build log when the build is finished!\n###############`, buildId, applicationId });
}
if (!debug && isCache) { if (!debug && isCache) {
await saveBuildLog({ await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`, line: `Debug turned off. To see more details, allow it in the configuration.`,
@ -529,16 +532,61 @@ export async function buildImage({
applicationId applicationId
}); });
} }
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const stream = await docker.engine.buildImage( const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
{ src: ['.'], context: workdir }, const { stderr } = await executeDockerCmd({ dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
{ if (debug) {
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation, const array = stderr.split('\n')
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}` for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
} }
); }
await streamEvents({ stream, docker, buildId, applicationId, debug });
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
// await new Promise((resolve, reject) => {
// const command = spawn(`docker`, ['build', '-f', `${workdir}${dockerFile}`, '-t', `${cache}`,`${workdir}`], {
// env: {
// DOCKER_HOST: 'ssh://root@95.217.178.202',
// DOCKER_BUILDKIT: '1'
// }
// });
// command.stdout.on('data', function (data) {
// console.log('stdout: ' + data);
// });
// command.stderr.on('data', function (data) {
// console.log('stderr: ' + data);
// });
// command.on('error', function (error) {
// console.log(error)
// reject(error)
// })
// command.on('exit', function (code) {
// console.log('exit code: ' + code);
// resolve(code)
// });
// })
// console.log({ stdout, stderr })
// const stream = await docker.engine.buildImage(
// { src: ['.'], context: workdir },
// {
// dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
// t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
// }
// );
// await streamEvents({ stream, docker, buildId, applicationId, debug });
if (isCache) {
await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId });
} else {
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
}
} }
export async function streamEvents({ stream, docker, buildId, applicationId, debug }) { export async function streamEvents({ stream, docker, buildId, applicationId, debug }) {
@ -617,18 +665,16 @@ export function makeLabelForStandaloneApplication({
export async function buildCacheImageWithNode(data, imageForBuild) { export async function buildCacheImageWithNode(data, imageForBuild) {
const { const {
applicationId,
tag,
workdir, workdir,
docker,
buildId, buildId,
baseDirectory, baseDirectory,
installCommand, installCommand,
buildCommand, buildCommand,
debug,
secrets, secrets,
pullmergeRequestId pullmergeRequestId
} = data; } = data;
const isPnpm = checkPnpm(installCommand, buildCommand); const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
@ -659,11 +705,12 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ ...data, isCache: true });
} }
export async function buildCacheImageForLaravel(data, imageForBuild) { export async function buildCacheImageForLaravel(data, imageForBuild) {
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data; const { workdir, buildId, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
@ -687,22 +734,16 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
Dockerfile.push(`COPY resources /app/resources`); Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`); Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ ...data, isCache: true });
} }
export async function buildCacheImageWithCargo(data, imageForBuild) { export async function buildCacheImageWithCargo(data, imageForBuild) {
const { const {
applicationId, applicationId,
tag,
workdir, workdir,
docker,
buildId, buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
@ -717,5 +758,5 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`); Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ ...data, isCache: true });
} }

View File

@ -1,18 +1,18 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { buildImage } from './common'; import { buildImage } from './common';
export default async function ({ export default async function (data) {
applicationId, let {
debug, applicationId,
tag, debug,
workdir, tag,
docker, workdir,
buildId, buildId,
baseDirectory, baseDirectory,
secrets, secrets,
pullmergeRequestId, pullmergeRequestId,
dockerFileLocation dockerFileLocation
}) { } = data
try { try {
const file = `${workdir}${dockerFileLocation}`; const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`; let dockerFileOut = `${workdir}`;
@ -45,7 +45,7 @@ export default async function ({
} }
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n')); await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation }); await buildImage(data);
} catch (error) { } catch (error) {
throw error; throw error;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,43 @@
import { asyncExecShell } from './common'; import { executeDockerCmd } from './common';
import Dockerode from 'dockerode';
export function getEngine(engine: string): string {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
}
export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } {
return {
engine: new Dockerode({
socketPath: destinationDocker.engine
}),
network: destinationDocker.network
};
}
export async function checkContainer(engine: string, container: string, remove = false): Promise<boolean> { export function formatLabelsOnDocker(data) {
const host = getEngine(engine); return data.trim().split('\n').map(a => JSON.parse(a)).map((container) => {
const labels = container.Labels.split(',')
let jsonLabels = {}
labels.forEach(l => {
const name = l.split('=')[0]
const value = l.split('=')[1]
jsonLabels = { ...jsonLabels, ...{ [name]: value } }
})
container.Labels = jsonLabels;
return container
})
}
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> {
let containerFound = false; let containerFound = false;
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await executeDockerCmd({
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}` dockerId,
); command:
`docker inspect --format '{{json .State}}' ${container}`
});
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = status === 'running'; const isRunning = status === 'running';
if (status === 'created') { if (status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); await executeDockerCmd({
dockerId,
command:
`docker rm ${container}`
});
} }
if (remove && status === 'exited') { if (remove && status === 'exited') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); await executeDockerCmd({
dockerId,
command:
`docker rm ${container}`
});
} }
if (isRunning) { if (isRunning) {
containerFound = true; containerFound = true;
@ -38,13 +48,10 @@ export async function checkContainer(engine: string, container: string, remove =
return containerFound; return containerFound;
} }
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> { export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
let isExited = false; let isExited = false;
const host = getEngine(engine);
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
if (stdout.trim() === 'exited') { if (stdout.trim() === 'exited') {
isExited = true; isExited = true;
} }
@ -57,19 +64,17 @@ export async function isContainerExited(engine: string, containerName: string):
export async function removeContainer({ export async function removeContainer({
id, id,
engine dockerId
}: { }: {
id: string; id: string;
engine: string; dockerId: string;
}): Promise<void> { }): Promise<void> {
const host = getEngine(engine);
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${id}`); await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${id}`); await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -5,12 +5,12 @@ import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { asyncExecShell, checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler'; import { scheduler } from '../../../../lib/scheduler';
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication } from './types'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
export async function listApplications(request: FastifyRequest) { export async function listApplications(request: FastifyRequest) {
@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) {
const { teamId } = request.user const { teamId } = request.user
const applications = await prisma.application.findMany({ const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true } include: { teams: true, destinationDocker: true }
}); });
const settings = await prisma.setting.findFirst() const settings = await prisma.setting.findFirst()
return { return {
@ -57,7 +57,28 @@ export async function getImages(request: FastifyRequest<GetImages>) {
} }
return { baseImage, baseBuildImage, baseBuildImages, baseImages, publishDirectory, port } return { baseBuildImage, baseBuildImages, publishDirectory, port }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
isExited = await isContainerExited(application.destinationDocker.id, id);
}
return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
@ -68,17 +89,9 @@ export async function getApplication(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
const { teamId } = request.user const { teamId } = request.user
const appId = process.env['COOLIFY_APP_ID']; const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return { return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
application, application,
appId appId
}; };
@ -279,16 +292,35 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
} }
} }
export async function stopPreviewApplication(request: FastifyRequest<StopPreviewApplication>, reply: FastifyReply) {
try {
const { id } = request.params
const { pullmergeRequestId } = request.body
const { teamId } = request.user
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const container = `${id}-${pullmergeRequestId}`
const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container });
if (found) {
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
}
}
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { teamId } = request.user const { teamId } = request.user
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) { if (application?.destinationDockerId) {
const { engine } = application.destinationDocker; const { id: dockerId } = application.destinationDocker;
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: application.destinationDocker.id });
} }
} }
return reply.code(201).send(); return reply.code(201).send();
@ -304,17 +336,17 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (application?.destinationDockerId && application.destinationDocker?.engine && application.destinationDocker?.network) { if (application?.destinationDockerId && application.destinationDocker?.network) {
const host = getEngine(application.destinationDocker.engine); const { stdout: containers } = await executeDockerCmd({
const { stdout: containers } = await asyncExecShell( dockerId: application.destinationDocker.id,
`DOCKER_HOST=${host} docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
); })
if (containers) { if (containers) {
const containersArray = containers.trim().split('\n'); const containersArray = containers.trim().split('\n');
for (const container of containersArray) { for (const container of containersArray) {
const containerObj = JSON.parse(container); const containerObj = JSON.parse(container);
const id = containerObj.ID; const id = containerObj.ID;
await removeContainer({ id, engine: application.destinationDocker.engine }); await removeContainer({ id, dockerId: application.destinationDocker.id });
} }
} }
} }
@ -333,33 +365,48 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function checkDomain(request: FastifyRequest<CheckDomain>) {
try {
const { id } = request.params
const { domain } = request.query
const { fqdn, settings: { dualCerts } } = await prisma.application.findUnique({ where: { id }, include: { settings: true } })
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkDNS(request: FastifyRequest<CheckDNS>) { export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try { try {
const { id } = request.params const { id } = request.params
let { exposePort, fqdn, forceSave, dualCerts } = request.body let { exposePort, fqdn, forceSave, dualCerts } = request.body
fqdn = fqdn.toLowerCase();
if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
const found = await isDomainConfigured({ id, fqdn });
const found = await isDomainConfigured({ id, fqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (exposePort) { if (exposePort) {
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) { if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
} }
const { default: getPort } = await import('get-port');
const publicPort = await getPort({ port: exposePort }); if (configuredPort !== exposePort) {
if (publicPort !== exposePort) { const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
throw { status: 500, message: `Port ${exposePort} is already in use.` } if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
} }
} }
if (isDNSCheckEnabled && !isDev && !forceSave) { if (isDNSCheckEnabled && !isDev && !forceSave) {
return await checkDomainsIsValidInDNS({ hostname: request.hostname.split(':')[0], fqdn, dualCerts }); let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress;
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
} }
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
@ -375,7 +422,7 @@ export async function getUsage(request) {
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
if (application.destinationDockerId) { if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]); [usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]);
} }
return { return {
usage usage
@ -701,21 +748,20 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
secret.value = decrypt(secret.value); secret.value = decrypt(secret.value);
return secret; return secret;
}); });
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret); const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret); const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const destinationDocker = await prisma.destinationDocker.findFirst({ const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
where: { application: { some: { id } }, teams: { some: { id: teamId } } } const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
}); if (stdout === '') {
const docker = dockerInstance({ destinationDocker }); return {
const listContainers = await docker.engine.listContainers({ containers: [],
filters: { network: [destinationDocker.network], name: [id] } applicationSecrets: [],
}); PRMRSecrets: []
const containers = listContainers.filter((container) => { }
return ( }
container.Labels['coolify.configuration'] && const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
container.Labels['coolify.type'] === 'standalone-application'
);
});
const jsonContainers = containers const jsonContainers = containers
.map((container) => .map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
@ -733,50 +779,46 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
}) })
} }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) { export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try { try {
const { id } = request.params const { id } = request.params;
let { since = 0 } = request.query let { since = 0 } = request.query
if (since !== 0) { if (since !== 0) {
since = day(since).unix(); since = day(since).unix();
} }
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({ const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try { try {
const container = await docker.engine.getContainer(id); // const found = await checkContainer({ dockerId, container: id })
if (container) { // if (found) {
const { default: ansi } = await import('strip-ansi') const { default: ansi } = await import('strip-ansi')
const logs = ( const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
await container.logs({ const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stdout: true, const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stderr: true, const logs = stripLogsStderr.concat(stripLogsStdout)
timestamps: true, const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
since, return { logs: sortedLogs }
tail: 5000 // }
}) } catch (error) {
) const { statusCode } = error;
.toString() if (statusCode === 404) {
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return { return {
logs logs: []
}; };
} }
} catch (error) {
return {
logs: []
};
} }
} }
return {
message: 'No logs found.'
}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }

View File

@ -1,8 +1,8 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -17,9 +17,14 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveApplication>('/:id', async (request, reply) => await saveApplication(request, reply)); fastify.post<SaveApplication>('/:id', async (request, reply) => await saveApplication(request, reply));
fastify.delete<DeleteApplication>('/:id', async (request, reply) => await deleteApplication(request, reply)); fastify.delete<DeleteApplication>('/:id', async (request, reply) => await deleteApplication(request, reply));
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));
fastify.post<SaveApplicationSettings>('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply)); fastify.post<SaveApplicationSettings>('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply));
fastify.get<CheckDomain>('/:id/check', async (request) => await checkDomain(request));
fastify.post<CheckDNS>('/:id/check', async (request) => await checkDNS(request)); fastify.post<CheckDNS>('/:id/check', async (request) => await checkDNS(request));
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request)); fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));

View File

@ -30,6 +30,9 @@ export interface SaveApplicationSettings extends OnlyId {
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
} }
export interface CheckDomain extends OnlyId {
Querystring: { domain: string; };
}
export interface CheckDNS extends OnlyId { export interface CheckDNS extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { Body: {
@ -115,3 +118,9 @@ export interface DeployApplication extends OnlyId {
branch: string branch: string
} }
} }
export interface StopPreviewApplication extends OnlyId {
Body: {
pullmergeRequestId: string | null,
}
}

View File

@ -1,10 +1,13 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { errorHandler, version } from '../../../../lib/common'; import { errorHandler, listSettings, version } from '../../../../lib/common';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async () => { fastify.get('/', async () => {
const settings = await listSettings()
try { try {
return { return {
ipv4: settings.ipv4,
ipv6: settings.ipv6,
version, version,
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true', whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON, whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,

View File

@ -3,24 +3,20 @@ import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { asyncExecShell, ComposeFile, createDirectories, decrypt, encrypt, errorHandler, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePort, listSettings, makeLabelForStandaloneDatabase, prisma, startTcpProxy, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { dockerInstance, getEngine } from '../../../../lib/docker'; import { checkContainer } from '../../../../lib/docker';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import { SaveDatabaseType } from './types'; import { SaveDatabaseType } from './types';
export async function listDatabases(request: FastifyRequest) { export async function listDatabases(request: FastifyRequest) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
let databases = [] const databases = await prisma.database.findMany({
if (teamId === '0') { where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
databases = await prisma.database.findMany({ include: { teams: true } }); include: { teams: true, destinationDocker: true }
} else { });
databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
return { return {
databases databases
} }
@ -56,6 +52,36 @@ export async function newDatabase(request: FastifyRequest, reply: FastifyReply)
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
let isRunning = false;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
const { destinationDockerId, destinationDocker } = database;
if (destinationDockerId) {
try {
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
return {
isRunning
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabase(request: FastifyRequest<OnlyId>) { export async function getDatabase(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params; const { id } = request.params;
@ -69,29 +95,11 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
} }
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { destinationDockerId, destinationDocker } = database;
let isRunning = false;
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
const configuration = generateDatabaseConfiguration(database); const configuration = generateDatabaseConfiguration(database);
const settings = await listSettings(); const settings = await listSettings();
return { return {
privatePort: configuration?.privatePort, privatePort: configuration?.privatePort,
database, database,
isRunning,
versions: await getDatabaseVersions(database.type), versions: await getDatabaseVersions(database.type),
settings settings
}; };
@ -164,16 +172,15 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
const { const {
destinationDockerId, destinationDockerId,
destinationDocker: { engine }, destinationDocker: { engine, id: dockerId },
version, version,
type type
} = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } }); } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } });
if (destinationDockerId) { if (destinationDockerId) {
const host = getEngine(engine);
if (type && version) { if (type && version) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type);
asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`); executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
} }
} }
return reply.code(201).send({}) return reply.code(201).send({})
@ -194,7 +201,7 @@ export async function getDatabaseUsage(request: FastifyRequest<OnlyId>) {
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) { if (database.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]); [usage] = await Promise.all([getContainerUsage(database.destinationDocker.id, id)]);
} }
return { return {
usage usage
@ -225,7 +232,6 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
generateDatabaseConfiguration(database); generateDatabaseConfiguration(database);
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const volumeName = volume.split(':')[0]; const volumeName = volume.split(':')[0];
const labels = await makeLabelForStandaloneDatabase({ id, image, volume }); const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
@ -267,13 +273,13 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${volumeName}`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
if (isPublic) await startTcpProxy(destinationDocker, id, publicPort, privatePort); if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {}; return {};
} catch (error) { } catch (error) {
throw { throw {
@ -311,39 +317,27 @@ export async function stopDatabase(request: FastifyRequest<OnlyId>) {
} }
export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>) { export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>) {
try { try {
const teamId = request.user.teamId;
const { id } = request.params; const { id } = request.params;
let { since = 0 } = request.query let { since = 0 } = request.query
if (since !== 0) { if (since !== 0) {
since = day(since).unix(); since = day(since).unix();
} }
const { destinationDockerId, destinationDocker } = await prisma.database.findUnique({ const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.database.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try { try {
const container = await docker.engine.getContainer(id); // const found = await checkContainer({ dockerId, container: id })
if (container) { // if (found) {
const { default: ansi } = await import('strip-ansi') const { default: ansi } = await import('strip-ansi')
const logs = ( const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
await container.logs({ const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stdout: true, const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stderr: true, const logs = stripLogsStderr.concat(stripLogsStdout)
timestamps: true, const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
since, return { logs: sortedLogs }
tail: 5000 // }
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
} catch (error) { } catch (error) {
const { statusCode } = error; const { statusCode } = error;
if (statusCode === 404) { if (statusCode === 404) {
@ -432,8 +426,10 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.params; const { id } = request.params;
const { isPublic, appendOnly = true } = request.body; const { isPublic, appendOnly = true } = request.body;
const publicPort = await getFreePort();
const settings = await listSettings(); const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
const publicPort = await getFreePublicPort(id, dockerId);
await prisma.database.update({ await prisma.database.update({
where: { id }, where: { id },
data: { data: {
@ -453,11 +449,7 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
if (destinationDockerId) { if (destinationDockerId) {
if (isPublic) { if (isPublic) {
await prisma.database.update({ where: { id }, data: { publicPort } }); await prisma.database.update({ where: { id }, data: { publicPort } });
if (settings.isTraefikUsed) { await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} else {
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
} else { } else {
await prisma.database.update({ where: { id }, data: { publicPort: null } }); await prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(id, destinationDocker, oldPublicPort); await stopTcpHttpProxy(id, destinationDocker, oldPublicPort);

View File

@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import type { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import type { SaveDatabaseType } from './types'; import type { SaveDatabaseType } from './types';
@ -15,6 +15,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply)); fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request)); fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request)); fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request)); fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));

View File

@ -1,14 +1,19 @@
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; import sshConfig from 'ssh-config'
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker'; import fs from 'fs/promises'
import os from 'os';
import { asyncExecShell, decrypt, errorHandler, executeDockerCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types'; import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
export async function listDestinations(request: FastifyRequest) { export async function listDestinations(request: FastifyRequest<ListDestinations>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { onlyVerified = false } = request.query
let destinations = [] let destinations = []
if (teamId === '0') { if (teamId === '0') {
destinations = await prisma.destinationDocker.findMany({ include: { teams: true } }); destinations = await prisma.destinationDocker.findMany({ include: { teams: true } });
@ -18,6 +23,9 @@ export async function listDestinations(request: FastifyRequest) {
include: { teams: true } include: { teams: true }
}); });
} }
if (onlyVerified) {
destinations = destinations.filter(destination => destination.engine || (destination.remoteEngine && destination.remoteVerified))
}
return { return {
destinations destinations
} }
@ -44,7 +52,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const destination = await prisma.destinationDocker.findFirst({ const destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { sshKey: true }
}); });
if (!destination && id !== 'new') { if (!destination && id !== 'new') {
throw { status: 404, message: `Destination not found.` }; throw { status: 404, message: `Destination not found.` };
@ -52,23 +61,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const settings = await listSettings(); const settings = await listSettings();
let payload = { let payload = {
destination, destination,
settings, settings
state: false
}; };
if (destination?.remoteEngine) {
// const { stdout } = await asyncExecShell(
// `ssh -p ${destination.port} ${destination.user}@${destination.ipAddress} "docker ps -a"`
// );
// console.log(stdout)
// const engine = await generateRemoteEngine(destination);
// // await saveSshKey(destination);
// payload.state = await checkContainer(engine, 'coolify-haproxy');
} else {
const containerName = 'coolify-proxy';
payload.state =
destination?.engine && (await checkContainer(destination.engine, containerName));
}
return { return {
...payload ...payload
}; };
@ -79,68 +73,68 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
} }
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) { export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try { try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed } = request.body
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (id === 'new') { const { id } = request.params
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (destinations.length > 0) { let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
const proxyConfigured = destinations.find( if (id === 'new') {
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true console.log(engine)
); if (engine) {
if (proxyConfigured) { const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed; if (stdout === '') {
await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`);
} }
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); await prisma.destinationDocker.create({
} data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
if (isCoolifyProxyUsed) { });
const settings = await prisma.setting.findFirst(); const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
if (settings?.isTraefikUsed) { const destination = destinations.find((destination) => destination.network === network);
await startTraefikProxy(engine); if (destinations.length > 0) {
} else { const proxyConfigured = destinations.find(
await startCoolifyProxy(engine); (destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
} }
if (isCoolifyProxyUsed) {
await startTraefikProxy(destination.id);
}
return reply.code(201).send({ id: destination.id });
} else {
const destination = await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort }
});
return reply.code(201).send({ id: destination.id })
} }
return reply.code(201).send({ id: destination.id });
} else { } else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send(); return reply.code(201).send();
} }
} catch ({ status, message }) { } catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteDestination(request: FastifyRequest<OnlyId>) { export async function deleteDestination(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
const destination = await prisma.destinationDocker.delete({ where: { id } }); const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } });
if (destination.isCoolifyProxyUsed) { if (isCoolifyProxyUsed) {
const host = getEngine(destination.engine); if (engine || remoteVerified) {
const { network } = destination; const { stdout: found } = await executeDockerCmd({
const settings = await prisma.setting.findFirst(); dockerId: id,
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy'; command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
const { stdout: found } = await asyncExecShell( })
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'` if (found) {
); await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
if (found) { await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` })
await asyncExecShell( }
`DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
);
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
} }
} }
await prisma.destinationDocker.delete({ where: { id } });
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -163,34 +157,105 @@ export async function saveDestinationSettings(request: FastifyRequest<SaveDestin
} }
} }
export async function startProxy(request: FastifyRequest<Proxy>) { export async function startProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body; const { id } = request.params
try { try {
await startTraefikProxy(engine); await startTraefikProxy(id);
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
await stopTraefikProxy(engine); console.log({ status, message })
await stopTraefikProxy(id);
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function stopProxy(request: FastifyRequest<Proxy>) { export async function stopProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body; const { id } = request.params
try { try {
await stopTraefikProxy(engine); await stopTraefikProxy(id);
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function restartProxy(request: FastifyRequest<Proxy>) { export async function restartProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body; const { id } = request.params
try { try {
await stopTraefikProxy(engine); await stopTraefikProxy(id);
await startTraefikProxy(engine); await startTraefikProxy(id);
await prisma.destinationDocker.updateMany({ await prisma.destinationDocker.update({
where: { engine }, where: { id },
data: { isCoolifyProxyUsed: true } data: { isCoolifyProxyUsed: true }
}); });
return {} return {}
} catch ({ status, message }) {
await prisma.destinationDocker.update({
where: { id },
data: { isCoolifyProxyUsed: false }
});
return errorHandler({ status, message })
}
}
export async function assignSSHKey(request: FastifyRequest) {
try {
const { id: sshKeyId } = request.body;
const { id } = request.params;
await prisma.destinationDocker.update({ where: { id }, data: { sshKey: { connect: { id: sshKeyId } } } })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const homedir = os.homedir();
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile(`/tmp/id_rsa_verification_${id}`, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
const host = `ssh://${remoteUser}@${remoteIpAddress}`
const config = sshConfig.parse('')
const found = config.find({ Host: remoteIpAddress })
if (!found) {
config.append({
Host: remoteIpAddress,
Port: remotePort.toString(),
User: remoteUser,
IdentityFile: `/tmp/id_rsa_verification_${id}`,
StrictHostKeyChecking: 'no'
})
}
try {
await fs.stat(`${homedir}/.ssh/`)
} catch (error) {
await fs.mkdir(`${homedir}/.ssh/`)
}
await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy' })
return {
isRunning
}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }

View File

@ -1,24 +1,29 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy } from './handlers'; import { assignSSHKey, checkDestination, deleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types'; import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify() return await request.jwtVerify()
}) })
fastify.get('/', async (request) => await listDestinations(request)); fastify.get<ListDestinations>('/', async (request) => await listDestinations(request));
fastify.post<CheckDestination>('/check', async (request) => await checkDestination(request)); fastify.post<CheckDestination>('/check', async (request) => await checkDestination(request));
fastify.get<OnlyId>('/:id', async (request) => await getDestination(request)); fastify.get<OnlyId>('/:id', async (request) => await getDestination(request));
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply)); fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request)); fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDestinationStatus(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request, reply) => await saveDestinationSettings(request)); fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request, reply) => await startProxy(request)); fastify.post<Proxy>('/:id/start', async (request,) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request, reply) => await stopProxy(request)); fastify.post<Proxy>('/:id/stop', async (request) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request, reply) => await restartProxy(request)); fastify.post<Proxy>('/:id/restart', async (request) => await restartProxy(request));
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
}; };
export default root; export default root;

View File

@ -1,5 +1,10 @@
import { OnlyId } from "../../../../types" import { OnlyId } from "../../../../types"
export interface ListDestinations {
Querystring: {
onlyVerified: string
}
}
export interface CheckDestination { export interface CheckDestination {
Body: { Body: {
network: string network: string
@ -20,7 +25,5 @@ export interface SaveDestinationSettings extends OnlyId {
} }
} }
export interface Proxy extends OnlyId { export interface Proxy extends OnlyId {
Body: {
engine: string
}
} }

View File

@ -17,7 +17,8 @@ export async function hashPassword(password: string): Promise<string> {
export async function cleanupManually() { export async function cleanupManually() {
try { try {
await cleanupDockerStorage('unix:///var/run/docker.sock', true, true) const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } })
await cleanupDockerStorage(destination.id, true, true)
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -154,7 +155,6 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
} }
if (userFound) { if (userFound) {
if (userFound.type === 'email') { if (userFound.type === 'email') {
// TODO: Review this one
if (userFound.password === 'RESETME') { if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {

View File

@ -273,11 +273,15 @@ export async function inviteToTeam(request: FastifyRequest<InviteToTeam>, reply:
const { email, permission, teamId, teamName } = request.body; const { email, permission, teamId, teamName } = request.body;
const userFound = await prisma.user.findUnique({ where: { email } }); const userFound = await prisma.user.findUnique({ where: { email } });
if (!userFound) { if (!userFound) {
throw `No user found with '${email}' email address.` throw {
message: `No user found with '${email}' email address.`
};
} }
const uid = userFound.id; const uid = userFound.id;
if (uid === userId) { if (uid === userId) {
throw `Invitation to yourself? Whaaaaat?` throw {
message: `Invitation to yourself? Whaaaaat?`
};
} }
const alreadyInTeam = await prisma.team.findFirst({ const alreadyInTeam = await prisma.team.findFirst({
where: { id: teamId, users: { some: { id: uid } } } where: { id: teamId, users: { some: { id: uid } } }

View File

@ -2,13 +2,13 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions } from '../../../../lib/common'; import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, dockerInstance, getEngine, removeContainer } from '../../../../lib/docker'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid'; import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
// async function startServiceNew(request: FastifyRequest<OnlyId>) { // async function startServiceNew(request: FastifyRequest<OnlyId>) {
// try { // try {
@ -145,15 +145,10 @@ import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteSer
export async function listServices(request: FastifyRequest) { export async function listServices(request: FastifyRequest) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
let services = [] const services = await prisma.service.findMany({
if (teamId === '0') { where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
services = await prisma.service.findMany({ include: { teams: true } }); include: { teams: true, destinationDocker: true }
} else { });
services = await prisma.service.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
return { return {
services services
} }
@ -172,43 +167,41 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
let isRunning = false;
let isExited = false
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service;
if (destinationDockerId) {
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
isExited = await isContainerExited(service.destinationDocker.id, id);
}
return {
isRunning,
isExited,
settings
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getService(request: FastifyRequest<OnlyId>) { export async function getService(request: FastifyRequest<OnlyId>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.params; const { id } = request.params;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const settings = await listSettings()
if (!service) { if (!service) {
throw { status: 404, message: 'Service not found.' } throw { status: 404, message: 'Service not found.' }
} }
const { destinationDockerId, destinationDocker, type, version, settings } = service;
let isRunning = false;
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
const docker = dockerInstance({ destinationDocker });
const baseImage = getServiceImage(type);
const images = getServiceImages(type);
docker.engine.pull(`${baseImage}:${version}`);
if (images?.length > 0) {
for (const image of images) {
docker.engine.pull(`${image}:latest`);
}
}
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
return { return {
isRunning,
service, service,
settings settings
} }
@ -282,7 +275,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
if (service.destinationDockerId) { if (service.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]); [usage] = await Promise.all([getContainerUsage(service.destinationDocker.id, id)]);
} }
return { return {
usage usage
@ -299,33 +292,22 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
if (since !== 0) { if (since !== 0) {
since = day(since).unix(); since = day(since).unix();
} }
const { destinationDockerId, destinationDocker } = await prisma.service.findUnique({ const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.service.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try { try {
const container = await docker.engine.getContainer(id); // const found = await checkContainer({ dockerId, container: id })
if (container) { // if (found) {
const { default: ansi } = await import('strip-ansi') const { default: ansi } = await import('strip-ansi')
const logs = ( const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
await container.logs({ const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stdout: true, const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
stderr: true, const logs = stripLogsStderr.concat(stripLogsStdout)
timestamps: true, const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
since, return { logs: sortedLogs }
tail: 5000 // }
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
} catch (error) { } catch (error) {
const { statusCode } = error; const { statusCode } = error;
if (statusCode === 404) { if (statusCode === 404) {
@ -364,40 +346,57 @@ export async function saveServiceSettings(request: FastifyRequest<SaveServiceSet
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function checkServiceDomain(request: FastifyRequest<CheckServiceDomain>) {
try {
const { id } = request.params
const { domain } = request.query
const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id } })
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkService(request: FastifyRequest<CheckService>) { export async function checkService(request: FastifyRequest<CheckService>) {
try { try {
const { id } = request.params; const { id } = request.params;
let { fqdn, exposePort, otherFqdns } = request.body; let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
let found = await isDomainConfigured({ id, fqdn }); const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
let found = await isDomainConfigured({ id, fqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (otherFqdns && otherFqdns.length > 0) { if (otherFqdns && otherFqdns.length > 0) {
for (const ofqdn of otherFqdns) { for (const ofqdn of otherFqdns) {
found = await isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true }); found = await isDomainConfigured({ id, fqdn: ofqdn, dockerId });
if (found) { if (found) {
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
} }
} }
} }
if (exposePort) { if (exposePort) {
const { default: getPort } = await import('get-port');
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) { if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
} }
const publicPort = await getPort({ port: exposePort }); if (configuredPort !== exposePort) {
if (publicPort !== exposePort) { const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
throw { status: 500, message: `Port ${exposePort} is already in use.` } if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
} }
} }
if (isDNSCheckEnabled && !isDev && !forceSave) {
let hostname = request.hostname.split(':')[0];
if (remoteEngine) hostname = remoteIpAddress;
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
}
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -742,7 +741,6 @@ async function startPlausibleAnalyticsService(request: FastifyRequest<ServiceSta
}); });
} }
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('plausibleanalytics'); const port = getServiceMainPort('plausibleanalytics');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -859,10 +857,8 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell( await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
);
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -877,17 +873,17 @@ async function stopPlausibleAnalyticsService(request: FastifyRequest<ServiceStar
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const engine = destinationDocker.engine;
let found = await checkContainer(engine, id); let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
found = await checkContainer(engine, `${id}-postgresql`); found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) { if (found) {
await removeContainer({ id: `${id}-postgresql`, engine }); await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
} }
found = await checkContainer(engine, `${id}-clickhouse`); found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-clickhouse` });
if (found) { if (found) {
await removeContainer({ id: `${id}-clickhouse`, engine }); await removeContainer({ id: `${id}-clickhouse`, dockerId: destinationDocker.id });
} }
} }
@ -905,7 +901,6 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('nocodb'); const port = getServiceMainPort('nocodb');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -956,8 +951,8 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -970,10 +965,9 @@ async function stopNocodbService(request: FastifyRequest<ServiceStartStop>) {
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
const found = await checkContainer(engine, id);
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} }
return {} return {}
@ -999,10 +993,10 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('minio'); const port = getServiceMainPort('minio');
const publicPort = await getFreePort(); const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const publicPort = await getFreePublicPort(id, dockerId);
const consolePort = 9001; const consolePort = 9001;
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1058,8 +1052,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
@ -1071,13 +1065,12 @@ async function stopMinioService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort: null } }) await prisma.minio.update({ where: { serviceId: id }, data: { publicPort: null } })
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
const found = await checkContainer(engine, id);
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} }
return {} return {}
@ -1103,7 +1096,6 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vscodeserver'); const port = getServiceMainPort('vscodeserver');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1175,16 +1167,16 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
const changePermissionOn = persistentStorage.map((p) => p.path); const changePermissionOn = persistentStorage.map((p) => p.path);
if (changePermissionOn.length > 0) { if (changePermissionOn.length > 0) {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' ' ' '
)}` )}`
); })
} }
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
@ -1196,12 +1188,11 @@ async function stopVscodeService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
const found = await checkContainer(engine, id);
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} }
return {} return {}
@ -1236,7 +1227,6 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const image = getServiceImage(type); const image = getServiceImage(type);
const port = getServiceMainPort('wordpress'); const port = getServiceMainPort('wordpress');
@ -1328,8 +1318,10 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
} }
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1346,28 +1338,27 @@ async function stopWordpressService(request: FastifyRequest<ServiceStartStop>) {
wordpress: { ftpEnabled } wordpress: { ftpEnabled }
} = service; } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
const found = await checkContainer(engine, `${id}-mysql`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mysql` });
if (found) { if (found) {
await removeContainer({ id: `${id}-mysql`, engine }); await removeContainer({ id: `${id}-mysql`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
if (ftpEnabled) { if (ftpEnabled) {
const found = await checkContainer(engine, `${id}-ftp`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (found) { if (found) {
await removeContainer({ id: `${id}-ftp`, engine }); await removeContainer({ id: `${id}-ftp`, dockerId: destinationDocker.id });
} }
await prisma.wordpress.update({ await prisma.wordpress.update({
where: { serviceId: id }, where: { serviceId: id },
@ -1393,7 +1384,6 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vaultwarden'); const port = getServiceMainPort('vaultwarden');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1444,8 +1434,10 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1456,14 +1448,12 @@ async function stopVaultwardenService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1483,7 +1473,6 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('languagetool'); const port = getServiceMainPort('languagetool');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1536,8 +1525,9 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1548,14 +1538,12 @@ async function stopLanguageToolService(request: FastifyRequest<ServiceStartStop>
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1575,7 +1563,6 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('n8n'); const port = getServiceMainPort('n8n');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1628,8 +1615,10 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1640,14 +1629,12 @@ async function stopN8nService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1667,7 +1654,6 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('uptimekuma'); const port = getServiceMainPort('uptimekuma');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1719,8 +1705,9 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1731,14 +1718,12 @@ async function stopUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1774,7 +1759,6 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
} }
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type); const image = getServiceImage(type);
@ -1871,8 +1855,9 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1883,18 +1868,16 @@ async function stopGhostService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
let found = await checkContainer(engine, id); let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
found = await checkContainer(engine, `${id}-mariadb`); found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` });
if (found) { if (found) {
await removeContainer({ id: `${id}-mariadb`, engine }); await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -1917,7 +1900,6 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service; service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('meilisearch'); const port = getServiceMainPort('meilisearch');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1972,8 +1954,10 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -1984,14 +1968,12 @@ async function stopMeilisearchService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -2024,7 +2006,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
} }
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('umami'); const port = getServiceMainPort('umami');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2191,8 +2172,10 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -2203,22 +2186,20 @@ async function stopUmamiService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
const found = await checkContainer(engine, `${id}-postgresql`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) { if (found) {
await removeContainer({ id: `${id}-postgresql`, engine }); await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -2245,7 +2226,6 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('hasura'); const port = getServiceMainPort('hasura');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2327,8 +2307,9 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -2339,22 +2320,20 @@ async function stopHasuraService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
const found = await checkContainer(engine, `${id}-postgresql`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) { if (found) {
await removeContainer({ id: `${id}-postgresql`, engine }); await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -2396,7 +2375,6 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
} }
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('fider'); const port = getServiceMainPort('fider');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2489,8 +2467,8 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
@ -2502,22 +2480,20 @@ async function stopFiderService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
const found = await checkContainer(engine, `${id}-postgresql`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) { if (found) {
await removeContainer({ id: `${id}-postgresql`, engine }); await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -2554,12 +2530,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
} }
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('moodle'); const port = getServiceMainPort('moodle');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type); const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = { const config = {
moodle: { moodle: {
image: `${image}:${version}`, image: `${image}:${version}`,
@ -2652,8 +2626,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); //await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
@ -2665,22 +2641,20 @@ async function stopMoodleService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service; const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine;
try { try {
const found = await checkContainer(engine, id); const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) { if (found) {
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try { try {
const found = await checkContainer(engine, `${id}-mariadb`); const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` });
if (found) { if (found) {
await removeContainer({ id: `${id}-mariadb`, engine }); await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -2703,14 +2677,10 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase } plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = await getServiceFromDB({ id, teamId }); } = await getServiceFromDB({ id, teamId });
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker }); await executeDockerCmd({
const container = await docker.engine.getContainer(id); dockerId: destinationDocker.id,
const command = await container.exec({ command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
Cmd: [ })
`psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
]
});
await command.start();
return await reply.code(201).send() return await reply.code(201).send()
} }
throw { status: 500, message: 'Could not activate users.' } throw { status: 500, message: 'Could not activate users.' }
@ -2722,7 +2692,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
const { id } = request.params const { id } = request.params
const { ftpEnabled } = request.body; const { ftpEnabled } = request.body;
const publicPort = await getFreePort(); const { service: { destinationDocker: { id: dockerId } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
const publicPort = await getFreePublicPort(id, dockerId);
let ftpUser = cuid(); let ftpUser = cuid();
let ftpPassword = generatePassword(); let ftpPassword = generatePassword();
@ -2742,7 +2715,6 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
ftpHostKeyPrivate ftpHostKeyPrivate
} = data; } = data;
const { network, engine } = destinationDocker; const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) { if (ftpEnabled) {
if (user) ftpUser = user; if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword); if (savedPassword) ftpPassword = decrypt(savedPassword);
@ -2789,7 +2761,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
}); });
try { try {
const isRunning = await checkContainer(engine, `${id}-ftp`); const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (isRunning) { if (isRunning) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
@ -2841,9 +2813,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
); );
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`); await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose)); await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` dockerId: destinationDocker.id,
); command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
})
} }
return reply.code(201).send({ return reply.code(201).send({
publicPort, publicPort,
@ -2856,9 +2830,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
data: { ftpPublicPort: null } data: { ftpPublicPort: null }
}); });
try { try {
await asyncExecShell( await executeDockerCmd({
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` dockerId: destinationDocker.id,
); command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
})
} catch (error) { } catch (error) {
// //
} }

View File

@ -3,12 +3,14 @@ import {
activatePlausibleUsers, activatePlausibleUsers,
activateWordpressFtp, activateWordpressFtp,
checkService, checkService,
checkServiceDomain,
deleteService, deleteService,
deleteServiceSecret, deleteServiceSecret,
deleteServiceStorage, deleteServiceStorage,
getService, getService,
getServiceLogs, getServiceLogs,
getServiceSecrets, getServiceSecrets,
getServiceStatus,
getServiceStorages, getServiceStorages,
getServiceType, getServiceType,
getServiceUsage, getServiceUsage,
@ -28,7 +30,7 @@ import {
} from './handlers'; } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@ -41,6 +43,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply)); fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request)); fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getServiceStatus(request));
fastify.get<CheckServiceDomain>('/:id/check', async (request) => await checkServiceDomain(request));
fastify.post<CheckService>('/:id/check', async (request) => await checkService(request)); fastify.post<CheckService>('/:id/check', async (request) => await checkService(request));
fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply)); fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));

View File

@ -25,9 +25,16 @@ export interface SaveServiceSettings extends OnlyId {
dualCerts: boolean dualCerts: boolean
} }
} }
export interface CheckServiceDomain extends OnlyId {
Querystring: {
domain: string
}
}
export interface CheckService extends OnlyId { export interface CheckService extends OnlyId {
Body: { Body: {
fqdn: string, fqdn: string,
forceSave: boolean,
dualCerts: boolean,
exposePort: number, exposePort: number,
otherFqdns: Array<string> otherFqdns: Array<string>
} }

View File

@ -1,15 +1,24 @@
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types'; import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) { export async function listAllSettings(request: FastifyRequest) {
try { try {
const teamId = request.user.teamId;
const settings = await listSettings(); const settings = await listSettings();
const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } })
const unencryptedKeys = []
if (sshKeys.length > 0) {
for (const key of sshKeys) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
}
}
return { return {
settings settings,
sshKeys: unencryptedKeys
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -68,7 +77,8 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
throw "Domain already configured"; throw "Domain already configured";
} }
if (isDNSCheckEnabled && !forceSave) { if (isDNSCheckEnabled && !forceSave) {
return await checkDomainsIsValidInDNS({ hostname: request.hostname.split(':')[0], fqdn, dualCerts }); const hostname = request.hostname.split(':')[0]
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
} }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
@ -84,3 +94,30 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { privateKey, name } = request.body;
const found = await prisma.sshKey.findMany({ where: { name } })
if (found.length > 0) {
throw {
message: "Name already used. Choose another one please."
}
}
const encryptedSSHKey = encrypt(privateKey)
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers'; import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types'; import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@ -13,6 +13,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request)); fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request)); fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
}; };
export default root; export default root;

View File

@ -29,3 +29,14 @@ export interface CheckDNS {
domain: string, domain: string,
} }
} }
export interface SaveSSHKey {
Body: {
privateKey: string,
name: string
}
}
export interface DeleteSSHKey {
Body: {
id: string
}
}

View File

@ -75,18 +75,18 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
if (!allowedGithubEvents.includes(githubEvent)) { if (!allowedGithubEvents.includes(githubEvent)) {
throw { status: 500, message: 'Event not allowed.' } throw { status: 500, message: 'Event not allowed.' }
} }
let repository, projectId, branch; let projectId, branch;
const body = request.body const body = request.body
if (githubEvent === 'push') { if (githubEvent === 'push') {
repository = body.repository; projectId = body.repository.id;
projectId = repository.id; branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
branch = body.ref.split('/')[2];
} else if (githubEvent === 'pull_request') { } else if (githubEvent === 'pull_request') {
repository = body.pull_request.head.repo; projectId = body.pull_request.base.repo.id;
projectId = repository.id; branch = body.pull_request.base.ref.includes('/') ? body.pull_request.base.ref.split('/')[2] : body.pull_request.base.ref;
branch = body.pull_request.head.ref.split('/')[2]; }
if (!projectId || !branch) {
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
} }
const applicationFound = await getApplicationFromDBWebhook(projectId, branch); const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
if (applicationFound) { if (applicationFound) {
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null; const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
@ -154,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (githubEvent === 'pull_request') { } else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number; const pullmergeRequestId = body.number;
const pullmergeRequestAction = body.action; const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref; const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
if (!allowedActions.includes(pullmergeRequestAction)) { if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' } throw { status: 500, message: 'Action not allowed.' }
} }
@ -162,8 +162,10 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
if (applicationFound.settings.previews) { if (applicationFound.settings.previews) {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const isRunning = await checkContainer( const isRunning = await checkContainer(
applicationFound.destinationDocker.engine, {
applicationFound.id dockerId: applicationFound.destinationDocker.id,
container: applicationFound.id
}
); );
if (!isRunning) { if (!isRunning) {
throw { status: 500, message: 'Application not running.' } throw { status: 500, message: 'Application not running.' }
@ -204,8 +206,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (pullmergeRequestAction === 'closed') { } else if (pullmergeRequestAction === 'closed') {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`; const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine; await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
await removeContainer({ id, engine });
} }
return { return {
message: 'Removed preview. Thank you!' message: 'Removed preview. Thank you!'

View File

@ -8,12 +8,22 @@ export interface GitHubEvents {
Body: { Body: {
number: string, number: string,
action: string, action: string,
repository: string, repository: {
id: string,
},
ref: string, ref: string,
pull_request: { pull_request: {
base: {
ref: string,
repo: {
id: string,
}
},
head: { head: {
ref: string, ref: string,
repo: string repo: {
id: string,
}
} }
} }
} }

View File

@ -34,7 +34,6 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
} }
return reply.redirect(`/webhooks/success?token=${data.access_token}`) return reply.redirect(`/webhooks/success?token=${data.access_token}`)
} catch ({ status, message, ...other }) { } catch ({ status, message, ...other }) {
console.log(other)
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
@ -117,8 +116,10 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (applicationFound.settings.previews) { if (applicationFound.settings.previews) {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const isRunning = await checkContainer( const isRunning = await checkContainer(
applicationFound.destinationDocker.engine, {
applicationFound.id dockerId: applicationFound.destinationDocker.id,
container: applicationFound.id
}
); );
if (!isRunning) { if (!isRunning) {
throw { status: 500, message: 'Application not running.' } throw { status: 500, message: 'Application not running.' }
@ -164,7 +165,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`; const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine; const engine = applicationFound.destinationDocker.engine;
await removeContainer({ id, engine }); await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
} }
return { return {
message: 'Removed preview. Thank you!' message: 'Removed preview. Thank you!'

View File

@ -1,6 +1,5 @@
import { FastifyRequest } from "fastify"; import { FastifyRequest } from "fastify";
import { asyncExecShell, errorHandler, getDomain, isDev, listServicesWithIncludes, prisma, supportedServiceTypesAndVersions } from "../../../lib/common"; import { errorHandler, getDomain, isDev, prisma, supportedServiceTypesAndVersions, include, executeDockerCmd } from "../../../lib/common";
import { getEngine } from "../../../lib/docker";
import { TraefikOtherConfiguration } from "./types"; import { TraefikOtherConfiguration } from "./types";
function configureMiddleware( function configureMiddleware(
@ -167,6 +166,7 @@ export async function traefikConfiguration(request, reply) {
} }
}; };
const applications = await prisma.application.findMany({ const applications = await prisma.application.findMany({
where: { destinationDocker: { remoteEngine: false } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
const data = { const data = {
@ -184,7 +184,7 @@ export async function traefikConfiguration(request, reply) {
settings: { previews, dualCerts } settings: { previews, dualCerts }
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { engine, network } = destinationDocker; const { network, id: dockerId } = destinationDocker;
const isRunning = true; const isRunning = true;
if (fqdn) { if (fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
@ -205,10 +205,7 @@ export async function traefikConfiguration(request, reply) {
}); });
} }
if (previews) { if (previews) {
const host = getEngine(engine); const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout const containers = stdout
.trim() .trim()
.split('\n') .split('\n')
@ -235,7 +232,11 @@ export async function traefikConfiguration(request, reply) {
} }
} }
} }
const services = await listServicesWithIncludes(); const services: any = await prisma.service.findMany({
where: { destinationDocker: { remoteEngine: false } },
include,
orderBy: { createdAt: 'desc' },
});
for (const service of services) { for (const service of services) {
const { const {
@ -248,7 +249,6 @@ export async function traefikConfiguration(request, reply) {
plausibleAnalytics plausibleAnalytics
} = service; } = service;
if (destinationDockerId) { if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const port = found.ports.main; const port = found.ports.main;
@ -488,3 +488,218 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function remoteTraefikConfiguration(request: FastifyRequest) {
const { id } = request.params
try {
const traefik = {
http: {
routers: {},
services: {},
middlewares: {
'redirect-to-https': {
redirectscheme: {
scheme: 'https'
}
},
'redirect-to-http': {
redirectscheme: {
scheme: 'http'
}
},
'redirect-to-non-www': {
redirectregex: {
regex: '^https?://www\\.(.+)',
replacement: 'http://${1}'
}
},
'redirect-to-www': {
redirectregex: {
regex: '^https?://(?:www\\.)?(.+)',
replacement: 'http://www.${1}'
}
}
}
}
};
const applications = await prisma.application.findMany({
where: { destinationDocker: { id } },
include: { destinationDocker: true, settings: true }
});
const data = {
applications: [],
services: [],
coolify: []
};
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews, dualCerts }
} = application;
if (destinationDockerId) {
const { id: dockerId, network } = destinationDocker;
const isRunning = true;
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
if (isRunning) {
data.applications.push({
id,
container: id,
port: port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
if (previews) {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
data.applications.push({
id: container,
container,
port: port || 3000,
domain: previewDomain,
isRunning,
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
}
}
}
}
}
const services: any = await prisma.service.findMany({
where: { destinationDocker: { id } },
include,
orderBy: { createdAt: 'desc' }
});
for (const service of services) {
const {
fqdn,
id,
type,
destinationDocker,
destinationDockerId,
dualCerts,
plausibleAnalytics
} = service;
if (destinationDockerId) {
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = true;
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
if (isRunning) {
// Plausible Analytics custom script
let scriptName = false;
if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') {
scriptName = plausibleAnalytics.scriptName;
}
let container = id;
let otherDomain = null;
let otherNakedDomain = null;
let otherIsHttps = null;
let otherIsWWW = null;
if (type === 'minio' && service.minio.apiFqdn) {
otherDomain = getDomain(service.minio.apiFqdn);
otherNakedDomain = otherDomain.replace(/^www\./, '');
otherIsHttps = service.minio.apiFqdn.startsWith('https://');
otherIsWWW = service.minio.apiFqdn.includes('www.');
}
data.services.push({
id,
container,
type,
otherDomain,
otherNakedDomain,
otherIsHttps,
otherIsWWW,
port,
publicPort,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
scriptName
});
}
}
}
}
}
for (const application of data.applications) {
configureMiddleware(application, traefik);
}
for (const service of data.services) {
const { id, scriptName } = service;
configureMiddleware(service, traefik);
if (service.type === 'minio') {
service.id = id + '-minio';
service.container = id;
service.domain = service.otherDomain;
service.nakedDomain = service.otherNakedDomain;
service.isHttps = service.otherIsHttps;
service.isWWW = service.otherIsWWW;
service.port = 9000;
configureMiddleware(service, traefik);
}
if (scriptName) {
traefik.http.middlewares[`${id}-redir`] = {
replacepathregex: {
regex: `/js/${scriptName}`,
replacement: '/js/plausible.js'
}
};
}
}
for (const coolify of data.coolify) {
configureMiddleware(coolify, traefik);
}
if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null;
}
if (Object.keys(traefik.http.services).length === 0) {
traefik.http.services = null;
}
return {
...traefik
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -1,10 +1,12 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { traefikConfiguration, traefikOtherConfiguration } from './handlers'; import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
import { TraefikOtherConfiguration } from './types'; import { TraefikOtherConfiguration } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply)); fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request)); fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request));
}; };
export default root; export default root;

View File

@ -36,3 +36,4 @@ export interface SaveDatabaseSettings extends OnlyId {
} }

View File

@ -15,13 +15,13 @@
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.23.3", "@playwright/test": "1.23.4",
"@sveltejs/kit": "1.0.0-next.375", "@sveltejs/kit": "1.0.0-next.377",
"@types/js-cookie": "3.0.2", "@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.30.6", "@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6", "@typescript-eslint/parser": "5.30.6",
"autoprefixer": "10.4.7", "autoprefixer": "10.4.7",
"eslint": "8.19.0", "eslint": "8.20.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0", "eslint-plugin-svelte3": "4.0.0",
"postcss": "8.4.14", "postcss": "8.4.14",
@ -34,11 +34,11 @@
"tailwindcss-scrollbar": "0.1.0", "tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "4.7.4", "typescript": "4.7.4",
"vite": "^3.0.0" "vite": "3.0.1"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.36", "@sveltejs/adapter-static": "1.0.0-next.37",
"@zerodevx/svelte-toast": "0.7.2", "@zerodevx/svelte-toast": "0.7.2",
"cuid": "2.1.8", "cuid": "2.1.8",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",

View File

@ -250,7 +250,7 @@
"no_destination_found": "No destination found", "no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!", "new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": { "new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...", "saving_and_configuring_proxy": "Saving...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.", "install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination", "add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations" "predefined_destinations": "Predefined destinations"

View File

@ -1,6 +1,8 @@
import { writable, readable, type Writable, type Readable } from 'svelte/store'; import { writable, readable, type Writable, type Readable } from 'svelte/store';
interface AppSession { interface AppSession {
ipv4: string | null,
ipv6: string | null,
version: string | null, version: string | null,
userId: string | null, userId: string | null,
teamId: string | null, teamId: string | null,
@ -17,6 +19,8 @@ interface AppSession {
} }
export const loginEmail: Writable<string | undefined> = writable() export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
ipv4: null,
ipv6: null,
version: null, version: null,
userId: null, userId: null,
teamId: null, teamId: null,
@ -31,7 +35,6 @@ export const appSession: Writable<AppSession> = writable({
gitlab: null gitlab: null
} }
}); });
export const isTraefikUsed: Writable<boolean> = writable(false);
export const disabledButton: Writable<boolean> = writable(false); export const disabledButton: Writable<boolean> = writable(false);
export const status: Writable<any> = writable({ export const status: Writable<any> = writable({
application: { application: {
@ -41,14 +44,16 @@ export const status: Writable<any> = writable({
initialLoading: true initialLoading: true
}, },
service: { service: {
initialLoading: true, isRunning: false,
isExited: false,
loading: false, loading: false,
isRunning: false initialLoading: true
}, },
database: { database: {
initialLoading: true, isRunning: false,
isExited: false,
loading: false, loading: false,
isRunning: false initialLoading: true
} }
}); });
@ -60,7 +65,6 @@ export const features = readable({
export const location: Writable<null | string> = writable(null) export const location: Writable<null | string> = writable(null)
export const setLocation = (resource: any) => { export const setLocation = (resource: any) => {
console.log(GITPOD_WORKSPACE_URL)
if (GITPOD_WORKSPACE_URL && resource.exposePort) { if (GITPOD_WORKSPACE_URL && resource.exposePort) {
const { href } = new URL(GITPOD_WORKSPACE_URL); const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href const newURL = href

View File

@ -65,11 +65,12 @@
<script lang="ts"> <script lang="ts">
export let baseSettings: any; export let baseSettings: any;
$appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6;
$appSession.version = baseSettings.version; $appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled; $appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon; $appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
export let settings: any;
export let userId: string; export let userId: string;
export let teamId: string; export let teamId: string;
export let permission: string; export let permission: string;
@ -84,15 +85,13 @@
import UpdateAvailable from '$lib/components/UpdateAvailable.svelte'; import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
import PageLoader from '$lib/components/PageLoader.svelte'; import PageLoader from '$lib/components/PageLoader.svelte';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession, isTraefikUsed } from '$lib/store'; import { appSession } from '$lib/store';
if (userId) $appSession.userId = userId; if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId; if (teamId) $appSession.teamId = teamId;
if (permission) $appSession.permission = permission; if (permission) $appSession.permission = permission;
if (isAdmin) $appSession.isAdmin = isAdmin; if (isAdmin) $appSession.isAdmin = isAdmin;
$isTraefikUsed = settings?.isTraefikUsed || false;
async function logout() { async function logout() {
try { try {
Cookies.remove('token'); Cookies.remove('token');
@ -322,33 +321,31 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /> <path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg> </svg>
</a> </a>
{#if $appSession.teamId === '0'} <a
<a sveltekit:prefetch
sveltekit:prefetch href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
href="/settings" class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500" class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:text-yellow-500={$page.url.pathname.startsWith('/settings')} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')} data-tooltip="Settings"
data-tooltip="Settings" >
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<svg <path stroke="none" d="M0 0h24v24H0z" fill="none" />
xmlns="http://www.w3.org/2000/svg" <path
class="h-8 w-8" d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
viewBox="0 0 24 24" />
stroke-width="1.5" <circle cx="12" cy="12" r="3" />
stroke="currentColor" </svg>
fill="none" </a>
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</a>
{/if}
<div <div
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500" class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"

View File

@ -36,7 +36,6 @@
return { return {
props: { props: {
isQueueActive,
application application
}, },
stuff: { stuff: {
@ -53,7 +52,6 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
export let isQueueActive: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
@ -68,7 +66,7 @@
let loading = false; let loading = false;
let statusInterval: any; let statusInterval: any;
let isQueueActive= false;
$disabledButton = $disabledButton =
!$appSession.isAdmin || !$appSession.isAdmin ||
!application.fqdn || !application.fqdn ||
@ -119,7 +117,7 @@
async function getStatus() { async function getStatus() {
if ($status.application.loading) return; if ($status.application.loading) return;
$status.application.loading = true; $status.application.loading = true;
const data = await get(`/applications/${id}`); const data = await get(`/applications/${id}/status`);
isQueueActive = data.isQueueActive; isQueueActive = data.isQueueActive;
$status.application.isRunning = data.isRunning; $status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited; $status.application.isExited = data.isExited;

View File

@ -9,7 +9,7 @@
redirect: `/applications/${params.id}` redirect: `/applications/${params.id}`
}; };
} }
const response = await get(`/destinations`); const response = await get(`/destinations?onlyVerified=true`);
return { return {
props: { props: {
...response ...response
@ -65,7 +65,7 @@
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@ -41,7 +41,7 @@
import Setting from './_Setting.svelte'; import Setting from './_Setting.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning; $: isDisabled = !$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
@ -308,7 +308,7 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration --> <!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div> <div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
@ -536,7 +536,7 @@
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<Setting <Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isDisabled} disabled={$status.application.isRunning}
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}

View File

@ -106,7 +106,7 @@
{#if currentStatus === 'queued'} {#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div> <div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else} {:else}
<div class="flex justify-end sticky top-0 p-2"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500" class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"

View File

@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/applications/${params.id}/logs`);
return {
props: {
application: stuff.application,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts"> <script lang="ts">
export let application: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
let logs: any = []; let logs: any = [];
let lastLog: any = null; let lastLog: any = null;
@ -37,6 +18,8 @@
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
loadLogs(); loadLogs();
@ -48,6 +31,7 @@
}); });
async function loadAllLogs() { async function loadAllLogs() {
try { try {
logsLoading = true;
const data: any = await get(`/applications/${id}/logs`); const data: any = await get(`/applications/${id}/logs`);
if (data?.logs) { if (data?.logs) {
lastLog = data.logs[data.logs.length - 1]; lastLog = data.logs[data.logs.length - 1];
@ -56,9 +40,12 @@
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorNotification(error); return errorNotification(error);
} finally {
logsLoading = false;
} }
} }
async function loadLogs() { async function loadLogs() {
if (logsLoading) return;
try { try {
const newLogs: any = await get( const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}` `/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
@ -157,7 +144,7 @@
{#if loadLogsInterval} {#if loadLogsInterval}
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
<div class="flex justify-end sticky top-0 p-2 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent" class="bg-transparent"

View File

@ -1,12 +1,10 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => { export const load: Load = async ({ stuff, url }) => {
try { try {
const response = await get(`/applications/${params.id}/previews`);
return { return {
props: { props: {
application: stuff.application, application: stuff.application
...response
} }
}; };
} catch (error) { } catch (error) {
@ -19,10 +17,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let containers: any;
export let application: any; export let application: any;
export let PRMRSecrets: any;
export let applicationSecrets: any;
import Secret from './_Secret.svelte'; import Secret from './_Secret.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -31,12 +26,33 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte';
const { id } = $page.params; const { id } = $page.params;
let containers: any;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
removing: false
};
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`); const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets]; PRMRSecrets = [...data.secrets];
} }
async function removeApplication(container: any) {
try {
loading.removing = true;
await post(`/applications/${id}/stop/preview`, {
pullmergeRequestId: container.pullmergeRequestId
});
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
async function redeploy(container: any) { async function redeploy(container: any) {
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { const { buildId } = await post(`/applications/${id}/deploy`, {
@ -55,6 +71,19 @@
return errorNotification(error); return errorNotification(error);
} }
} }
onMount(async () => {
try {
loading.init = true;
const response = await get(`/applications/${id}/previews`);
containers = response.containers;
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) {
return errorNotification(error);
} finally {
loading.init = false;
}
});
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
@ -62,7 +91,7 @@
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments Preview Deployments
</div> </div>
<span class="text-xs">{application.name} </span> <span class="text-xs">{application?.name}</span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a <a
@ -108,68 +137,81 @@
</a> </a>
{/if} {/if}
</div> </div>
<div class="mx-auto max-w-6xl px-6 pt-4"> {#if loading.init}
<div class="flex justify-center py-4 text-center"> <Loading />
<Explainer {:else}
customClass="w-full" <div class="mx-auto max-w-6xl px-6 pt-4">
text={applicationSecrets.length === 0 <div class="flex justify-center py-4 text-center">
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments." <Explainer
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."} customClass="w-full"
/> text={applicationSecrets.length === 0
</div> ? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
{#if applicationSecrets.length !== 0} : "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
<table class="mx-auto border-separate text-left"> />
<thead> </div>
<tr class="h-12"> {#if applicationSecrets.length !== 0}
<th scope="col">{$t('forms.name')}</th> <table class="mx-auto border-separate text-left">
<th scope="col">{$t('forms.value')}</th> <thead>
<th scope="col" class="w-64 text-center" <tr class="h-12">
>{$t('application.preview.need_during_buildtime')}</th <th scope="col">{$t('forms.name')}</th>
> <th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th> <th scope="col" class="w-64 text-center"
</tr> >{$t('application.preview.need_during_buildtime')}</th
</thead> >
<tbody> <th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
{#each applicationSecrets as secret} </tr>
{#key secret.id} </thead>
<tr> <tbody>
<Secret {#each applicationSecrets as secret}
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)} {#key secret.id}
isPRMRSecret <tr>
name={secret.name} <Secret
value={secret.value} PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isBuildSecret={secret.isBuildSecret} isPRMRSecret
on:refresh={refreshSecrets} name={secret.name}
/> value={secret.value}
</tr> isBuildSecret={secret.isBuildSecret}
{/key} on:refresh={refreshSecrets}
{/each} />
</tbody> </tr>
</table> {/key}
{/if} {/each}
</div> </tbody>
</table>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>{$t('application.preview.redeploy')}</button
>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if} {/if}
</div> </div>
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
</div>
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>{$t('application.preview.redeploy')}</button
>
</div>
<div class="flex items-center justify-center">
<button
class:bg-red-600={!loading.removing}
class:hover:bg-red-500={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(container)}
>{loading.removing ? 'Removing...' : 'Remove Application'}
</button>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>
</div>
{/if}

View File

@ -93,7 +93,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application} {#each ownApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline"> <a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600"> <div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack} {#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'} {#if application.buildPack.toLowerCase() === 'rust'}
@ -140,15 +140,18 @@
{#if application.fqdn} {#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div> <div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if} {/if}
{#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div>
{/if}
{#if !application.gitSourceId || !application.repository || !application.branch} {#if !application.gitSourceId || !application.repository || !application.branch}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Git Source Missing Git Source Missing
</div> </div>
{:else if !application.destinationDockerId} {:else if !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing Destination Missing
</div> </div>
{:else if !application.fqdn} {:else if !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing URL Missing
</div> </div>
@ -158,10 +161,10 @@
{/each} {/each}
</div> </div>
{#if otherApplications.length > 0 && $appSession.teamId === '0'} {#if otherApplications.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div> <div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application} {#each otherApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline"> <a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600"> <div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack} {#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'} {#if application.buildPack.toLowerCase() === 'rust'}

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
export let database: any; export let database: any;
export let privatePort: any; export let privatePort: any;
export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@ -17,8 +16,9 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession, status } from '$lib/store'; import { appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
@ -49,12 +49,15 @@
databaseDbUser = ''; databaseDbUser = '';
} }
} }
function generateUrl(): string { function generateUrl(): string {
return `${database.type}://${ return `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : '' databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${ }${databaseDbUserPassword}@${
isPublic ? (settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname) : database.id isPublic
? database.destinationDocker.remoteEngine
? database.destinationDocker.remoteIpAddress
: $appSession.ipv4
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`; }:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
} }
@ -103,7 +106,7 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div> <div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
@ -153,7 +156,8 @@
> >
<input <input
value={database.version} value={database.version}
disabled={$status.database.isRunning} readonly
disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning} class:cursor-pointer={!$status.database.isRunning}
/></a /></a
> >
@ -203,9 +207,16 @@
<EdgeDB {database} /> <EdgeDB {database} />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100" <div>
>{$t('database.connection_string')}</label <label for="url" class="text-base font-bold text-stone-100"
> >{$t('database.connection_string')}</label
>
{#if !isPublic && database.destinationDocker.remoteEngine}
<Explainer
text="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}
</div>
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}

View File

@ -58,7 +58,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status } from '$lib/store'; import { appSession, status, disabledButton } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -66,6 +66,9 @@
let loading = false; let loading = false;
let statusInterval: any = false; let statusInterval: any = false;
$disabledButton = !$appSession.isAdmin;
async function deleteDatabase() { async function deleteDatabase() {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) { if (sure) {
@ -104,7 +107,7 @@
async function getStatus() { async function getStatus() {
if ($status.database.loading) return; if ($status.database.loading) return;
$status.database.loading = true; $status.database.loading = true;
const data = await get(`/databases/${id}`); const data = await get(`/databases/${id}/status`);
$status.database.isRunning = data.isRunning; $status.database.isRunning = data.isRunning;
$status.database.initialLoading = false; $status.database.initialLoading = false;
$status.database.loading = false; $status.database.loading = false;
@ -138,6 +141,32 @@
<Loading fullscreen cover /> <Loading fullscreen cover />
{:else} {:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} {#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if $status.database.isExited}
<a
href={!$disabledButton ? `/databases/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.database.initialLoading} {#if $status.database.initialLoading}
<button <button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"

View File

@ -9,7 +9,7 @@
redirect: `/database/${params.id}` redirect: `/database/${params.id}`
}; };
} }
const response = await get(`/destinations`); const response = await get(`/destinations?onlyVerified=true`);
return { return {
props: { props: {
...response ...response
@ -55,7 +55,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@ -15,7 +15,6 @@
import Databases from './_Databases/_Databases.svelte'; import Databases from './_Databases/_Databases.svelte';
import { status } from '$lib/store'; import { status } from '$lib/store';
export let database: any; export let database: any;
export let settings: any;
export let privatePort: any; export let privatePort: any;
const { id } = $page.params; const { id } = $page.params;
@ -86,4 +85,4 @@
</dl> </dl>
</div> </div>
</div> </div>
<Databases bind:database {privatePort} {settings} /> <Databases bind:database {privatePort}/>

View File

@ -1,31 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/databases/${params.id}/logs`);
return {
props: {
database: stuff.database,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts"> <script lang="ts">
export let database: any; import { onDestroy, onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte'; import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
const { id } = $page.params;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
let logs: any = []; let logs: any = [];
@ -34,9 +16,12 @@ import { errorNotification } from '$lib/common';
let followingLogs: any; let followingLogs: any;
let logsEl: any; let logsEl: any;
let position = 0; let position = 0;
let loadingLogs = false;
let database: any = {};
const { id } = $page.params;
onMount(async () => { onMount(async () => {
const { logs: firstLogs } = await get(`/databases/${id}/logs`);
logs = firstLogs;
loadAllLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
loadLogs(); loadLogs();
@ -48,6 +33,7 @@ import { errorNotification } from '$lib/common';
}); });
async function loadAllLogs() { async function loadAllLogs() {
try { try {
loadingLogs = true;
const data: any = await get(`/databases/${id}/logs`); const data: any = await get(`/databases/${id}/logs`);
if (data?.logs) { if (data?.logs) {
lastLog = data.logs[data.logs.length - 1]; lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,15 @@ import { errorNotification } from '$lib/common';
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorNotification(error); return errorNotification(error);
} finally {
loadingLogs = false;
} }
} }
async function loadLogs() { async function loadLogs() {
if (loadingLogs) return;
try { try {
const newLogs: any = await get( loadingLogs = true;
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}` const newLogs: any = await get(`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); logs = logs.concat(newLogs.logs);
@ -70,6 +58,8 @@ import { errorNotification } from '$lib/common';
} }
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
loadingLogs = false;
} }
} }
function detect() { function detect() {
@ -137,7 +127,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval} {#if loadLogsInterval}
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
<div class="flex justify-end sticky top-0 p-2 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent" class="bg-transparent"

View File

@ -78,7 +78,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDatabases as database} {#each ownDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline"> <a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600"> <div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'} {#if database.type === 'clickhouse'}
<Clickhouse isAbsolute /> <Clickhouse isAbsolute />
@ -103,6 +103,9 @@
{#if $appSession.teamId === '0' && otherDatabases.length > 0} {#if $appSession.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div> <div class="truncate text-center">{database.teams[0].name}</div>
{/if} {/if}
{#if database.destinationDocker?.name}
<div class="truncate text-center">{database.destinationDocker.name}</div>
{/if}
{#if !database.type} {#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')} {$t('application.configuration.configuration_missing')}
@ -113,10 +116,10 @@
{/each} {/each}
</div> </div>
{#if otherDatabases.length > 0 && $appSession.teamId === '0'} {#if otherDatabases.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div> <div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Databases</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDatabases as database} {#each otherDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline"> <a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600"> <div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'} {#if database.type === 'clickhouse'}
<Clickhouse isAbsolute /> <Clickhouse isAbsolute />

View File

@ -1,56 +1,65 @@
<script lang="ts"> <script lang="ts">
export let destination: any; export let destination: any;
export let settings: any; export let settings: any;
export let state: any;
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { post } from '$lib/api'; import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params; const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock'; let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false; let loading = {
let loadingProxy = false; restart: false,
let restarting = false; proxy: false,
save: false
};
async function handleSubmit() { async function handleSubmit() {
loading = true; loading.save = true;
try { try {
return await post(`/destinations/${id}`, { ...destination }); await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading.save = false;
} }
} }
onMount(async () => { onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) { loading.proxy = true;
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; const { isRunning } = await get(`/destinations/${id}/status`);
let proxyUsed = !destination.isCoolifyProxyUsed;
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
try { try {
await post(`/destinations/${id}/settings`, { await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: proxyUsed,
engine: destination.engine engine: destination.engine
}); });
await stopProxy(); await stopProxy();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
} else if (state === true && destination.isCoolifyProxyUsed === false) { } else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try { try {
await post(`/destinations/${id}/settings`, { await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: proxyUsed,
engine: destination.engine engine: destination.engine
}); });
await startProxy(); await startProxy();
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
loading.proxy = false;
} }
} }
loading.proxy = false;
}); });
async function changeProxySetting() { async function changeProxySetting() {
if (!cannotDisable) { if (!cannotDisable) {
@ -67,7 +76,7 @@
} }
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try { try {
loadingProxy = true; loading.proxy = true;
await post(`/destinations/${id}/settings`, { await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine engine: destination.engine
@ -80,7 +89,7 @@
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loadingProxy = false; loading.proxy = false;
} }
} }
} }
@ -104,7 +113,7 @@
const sure = confirm($t('destination.confirm_restart_proxy')); const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) { if (sure) {
try { try {
restarting = true; loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting')); toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart`, { await post(`/destinations/${id}/restart`, {
engine: destination.engine, engine: destination.engine,
@ -115,7 +124,7 @@
window.location.reload(); window.location.reload();
}, 5000); }, 5000);
} finally { } finally {
restarting = false; loading.restart = false;
} }
} }
} }
@ -128,23 +137,20 @@
<button <button
type="submit" type="submit"
class="bg-sky-600 hover:bg-sky-500" class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading} class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading} class:hover:bg-sky-500={!loading.save}
disabled={loading} disabled={loading.save}
>{loading ? $t('forms.saving') : $t('forms.save')} >{loading.save ? $t('forms.saving') : $t('forms.save')}
</button> </button>
<button <button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'} class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting} disabled={loading.restart}
on:click|preventDefault={forceRestartProxy} on:click|preventDefault={forceRestartProxy}
>{restarting >{loading.restart
? $t('destination.restarting_please_wait') ? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button : $t('destination.force_restart_proxy')}</button
> >
{/if} {/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div> </div>
<div class="grid grid-cols-2 items-center px-10 "> <div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@ -168,10 +174,6 @@
value={destination.engine} value={destination.engine}
/> />
</div> </div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField <CopyPasswordField
@ -186,7 +188,7 @@
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={loadingProxy} loading={loading.proxy}
disabled={cannotDisable} disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}

View File

@ -21,10 +21,9 @@
payload = { payload = {
name: $t('sources.remote_docker'), name: $t('sources.remote_docker'),
remoteEngine: true, remoteEngine: true,
ipAddress: null, remoteIpAddress: null,
user: 'root', remoteUser: 'root',
port: 22, remotePort: 22,
sshPrivateKey: null,
network: cuid(), network: cuid(),
isCoolifyProxyUsed: true isCoolifyProxyUsed: true
}; };
@ -44,7 +43,7 @@
<button class="w-32" on:click={() => setPredefined('localDocker')} <button class="w-32" on:click={() => setPredefined('localDocker')}
>{$t('sources.local_docker')}</button >{$t('sources.local_docker')}</button
> >
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> --> <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> --> <!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
</div> </div>
</div> </div>

View File

@ -5,23 +5,35 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
let loading = false; let loading = false;
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
try { try {
const { id } = await post('/new/destination/docker', { loading = true;
await post(`/destinations/check`, { network: payload.network });
const { id } = await post(`/destinations/new`, {
...payload ...payload
}); });
return await goto(`/destinations/${id}`); return await goto(`/destinations/${id}`);
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
<div class="text-center flex justify-center">
<Explainer
customClass="max-w-[32rem]"
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH connection</span> to initiate connection. You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker. <br>See <a class='text-white' href='https://docs.coollabs.io'>docs</a> for more details."
/>
</div>
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5"> <div class="flex items-center space-x-2 pb-5">
@ -44,40 +56,36 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="ipAddress" class="text-base font-bold text-stone-100" <label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label >{$t('forms.ip_address')}</label
> >
<input <input
required required
name="ipAddress" name="remoteIpAddress"
placeholder="{$t('forms.eg')}: 192.168..." placeholder="{$t('forms.eg')}: 192.168..."
bind:value={payload.ipAddress} bind:value={payload.remoteIpAddress}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} /> <input
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
>{$t('forms.ssh_private_key')}</label
>
<textarea
rows="10"
class="resize-none"
required required
name="sshPrivateKey" name="remoteUser"
placeholder="{$t('forms.eg')}: -----BEGIN...." placeholder="{$t('forms.eg')}: root"
bind:value={payload.sshPrivateKey} bind:value={payload.remoteUser}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input
required
name="remotePort"
placeholder="{$t('forms.eg')}: 22"
bind:value={payload.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input <input

View File

@ -1,80 +1,90 @@
<script lang="ts"> <script lang="ts">
export let destination: any; export let destination: any;
export let settings: any export let settings: any;
export let state: any
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { post } from '$lib/api'; import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, generateRemoteEngine } from '$lib/common'; import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock'; let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = []; let loading = {
let loading = false; restart: false,
let restarting = false; proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() { async function handleSubmit() {
loading = true; loading.save = true;
try { try {
return await post(`/destinations/${id}.json`, { ...destination }); await post(`/destinations/${id}`, { ...destination });
} catch (error ) { toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading.save = false;
} }
} }
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => { onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) { loading.proxy = true;
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; if (destination.remoteEngine && destination.remoteVerified) {
try { const { isRunning } = await get(`/destinations/${id}/status`);
await post(`/destinations/${id}/settings.json`, { if (isRunning === false && destination.isCoolifyProxyUsed === true) {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
engine: destination.engine try {
}); await post(`/destinations/${id}/settings`, {
await stopProxy(); isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
} catch (error ) { engine: destination.engine
return errorNotification(error); });
} await stopProxy();
} else if (state === true && destination.isCoolifyProxyUsed === false) { } catch (error) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; return errorNotification(error);
try { }
await post(`/destinations/${id}/settings.json`, { } else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
engine: destination.engine try {
}); await post(`/destinations/${id}/settings`, {
await startProxy(); isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
} catch ( error ) { engine: destination.engine
return errorNotification(error); });
await startProxy();
} catch (error) {
return errorNotification(error);
}
} }
} }
loading.proxy = false;
}); });
async function changeProxySetting() { async function changeProxySetting() {
loading.proxy = true;
if (!cannotDisable) { if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed; const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) { if (isProxyActivated) {
const sure = confirm( const sure = confirm(
`Are you sure you want to ${ `Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable' destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${ } Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
destination.engine
}'! Nothing will be reachable if you do it!`
); );
if (!sure) return; if (!sure) {
loading.proxy = false;
return;
}
} }
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; let proxyUsed = !destination.isCoolifyProxyUsed;
try { try {
await post(`/destinations/${id}/settings.json`, { await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: proxyUsed,
engine: destination.engine engine: destination.engine
}); });
if (isProxyActivated) { if (isProxyActivated) {
@ -82,15 +92,17 @@ import { appSession } from '$lib/store';
} else { } else {
await startProxy(); await startProxy();
} }
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
loading.proxy = false;
} }
} }
} }
async function stopProxy() { async function stopProxy() {
try { try {
const engine = generateRemoteEngine(destination); await post(`/destinations/${id}/stop`, { engine: destination.engine });
await post(`/destinations/${id}/stop.json`, { engine });
return toast.push($t('destination.coolify_proxy_stopped')); return toast.push($t('destination.coolify_proxy_stopped'));
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@ -98,8 +110,7 @@ import { appSession } from '$lib/store';
} }
async function startProxy() { async function startProxy() {
try { try {
const engine = generateRemoteEngine(destination); await post(`/destinations/${id}/start`, { engine: destination.engine });
await post(`/destinations/${id}/start.json`, { engine });
return toast.push($t('destination.coolify_proxy_started')); return toast.push($t('destination.coolify_proxy_started'));
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@ -109,9 +120,9 @@ import { appSession } from '$lib/store';
const sure = confirm($t('destination.confirm_restart_proxy')); const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) { if (sure) {
try { try {
restarting = true; loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting')); toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, { await post(`/destinations/${id}/restart`, {
engine: destination.engine, engine: destination.engine,
fqdn: settings.fqdn fqdn: settings.fqdn
}); });
@ -119,9 +130,24 @@ import { appSession } from '$lib/store';
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 5000); }, 5000);
} finally {
loading.restart = false;
} }
} }
} }
async function verifyRemoteDocker() {
try {
loading.verify = true;
await post(`/destinations/${id}/verify`, {});
destination.remoteVerified = true;
toast.push('Remote Docker Engine verified!');
return;
} catch (error) {
return errorNotification(error);
} finally {
loading.verify = false;
}
}
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
@ -131,23 +157,28 @@ import { appSession } from '$lib/store';
<button <button
type="submit" type="submit"
class="bg-sky-600 hover:bg-sky-500" class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading} class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading} class:hover:bg-sky-500={!loading.save}
disabled={loading} disabled={loading.save}
>{loading ? $t('forms.saving') : $t('forms.save')} >{loading.save ? $t('forms.saving') : $t('forms.save')}
</button> </button>
<button {#if !destination.remoteVerified}
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'} <button
disabled={restarting} disabled={loading.verify}
on:click|preventDefault={forceRestartProxy} on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{restarting >{loading.verify ? 'Verifying...' : 'Verify Remote Docker Engine'}</button
? $t('destination.restarting_please_wait') >
: $t('destination.force_restart_proxy')}</button {:else}
> <button
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{loading.restart
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
{/if} {/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div> </div>
<div class="grid grid-cols-2 items-center px-10 "> <div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
@ -159,22 +190,6 @@ import { appSession } from '$lib/store';
bind:value={destination.name} bind:value={destination.name}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label> <label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField <CopyPasswordField
@ -186,9 +201,53 @@ import { appSession } from '$lib/store';
value={destination.network} value={destination.network}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteIpAddress" class="text-base font-bold text-stone-100">IP Address</label>
<CopyPasswordField
id="remoteIpAddress"
readonly
disabled
name="remoteIpAddress"
value={destination.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField
id="remoteUser"
readonly
disabled
name="remoteUser"
value={destination.remoteUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
<CopyPasswordField
id="remotePort"
readonly
disabled
name="remotePort"
value={destination.remotePort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshKey" class="text-base font-bold text-stone-100">SSH Key</label>
<a
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
class="no-underline"
><input
value={destination.sshKey.name}
readonly
id="sshKey"
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
disabled={cannotDisable} disabled={cannotDisable}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')} title={$t('destination.use_coolify_proxy')}
@ -200,27 +259,3 @@ import { appSession } from '$lib/store';
/> />
</div> </div>
</form> </form>
<!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed}
{#if state}
<button on:click={stopProxy}>Stop proxy</button>
{:else}
<button on:click={startProxy}>Start proxy</button>
{/if}
{/if}
</div> -->
<!-- {#if scannedApps.length > 0}
<div class="flex justify-center px-6 pb-10">
<div class="flex space-x-2 h-8 items-center">
<div class="font-bold text-xl text-white">Found applications</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6">
<div class="flex space-x-2 justify-center">
{#each scannedApps as app}
<FoundApp {app} />
{/each}
</div>
</div>
{/if} -->

View File

@ -1,27 +1,46 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, url, params }) => { function checkConfiguration(destination: any): string | null {
let configurationPhase = null;
if (!destination?.remoteEngine) return configurationPhase;
if (!destination?.sshKey) {
configurationPhase = 'sshkey';
}
return configurationPhase;
}
export const load: Load = async ({ url, params }) => {
try { try {
const { id } = params; const { id } = params;
const response = await get(`/destinations/${id}`); const response = await get(`/destinations/${id}`);
const { destination, settings, state } = response; const { destination, settings } = response;
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) { if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
return { return {
status: 302, status: 302,
redirect: '/destinations' redirect: '/destinations'
}; };
} }
const configurationPhase = checkConfiguration(destination);
if (
configurationPhase &&
url.pathname !== `/destinations/${params.id}/configuration/${configurationPhase}`
) {
return {
status: 302,
redirect: `/destinations/${params.id}/configuration/${configurationPhase}`
};
}
return { return {
props: { props: {
destination destination
}, },
stuff: { stuff: {
destination, destination,
settings, settings
state
} }
}; };
} catch (error) { } catch (error) {
console.log(error)
return handlerNotFoundLoad(error, url); return handlerNotFoundLoad(error, url);
} }
}; };

View File

@ -0,0 +1,86 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/settings`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let sshKeys: any;
async function handleSubmit(sshKeyId: string) {
try {
await post(`/destinations/${id}/configuration/sshKey`, { id: sshKeyId });
return await goto(from || `/destinations/${id}`);
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a SSH Keys</div>
</div>
<div class="flex flex-col justify-center">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
{#if sshKeys.length > 0}
{#each sshKeys as sshKey}
<div class="p-2 relative">
<form on:submit|preventDefault={() => handleSubmit(sshKey.id)}>
<button
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
>
<div class="font-bold text-xl text-center truncate">{sshKey.name}</div>
</button>
</form>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="pb-2 text-center font-bold">No SSH key found</div>
<div class="flex justify-center">
<a
href="/settings/ssh-keys"
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500"
>
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
</div>
{/if}
</div>
</div>

View File

@ -39,7 +39,7 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div> <div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -66,24 +66,112 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination} {#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline"> <a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600"> <div class="box-selection hover:bg-sky-600 ">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div> <div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0' && otherDestinations.length > 0} {#if $appSession.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div> <div class="truncate text-center">{destination.teams[0].name}</div>
{/if} {/if}
<div class="truncate text-center">{destination.network}</div> <div class="truncate text-center">{destination.network}</div>
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
{/if}
{#if destination.remoteEngine && !destination.sshKeyId}
<div class="truncate text-center text-sm text-red-500">SSH Key missing!</div>
{/if}
</div> </div>
</a> </a>
{/each} {/each}
</div> </div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'} {#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div> <div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Destinations</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination} {#each otherDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline"> <a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<div class="box-selection hover:bg-sky-600"> <div class="box-selection hover:bg-sky-600">
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
/>
<path d="M5 10h3v3h-3z" />
<path d="M8 10h3v3h-3z" />
<path d="M11 10h3v3h-3z" />
<path d="M8 7h3v3h-3z" />
<path d="M11 7h3v3h-3z" />
<path d="M11 4h3v3h-3z" />
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
<line x1="10" y1="16" x2="10" y2="16.01" />
</svg>
{#if destination.remoteEngine}
<svg
xmlns="http://www.w3.org/2000/svg"
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="18" x2="12.01" y2="18" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
{/if}
<div class="truncate text-center text-xl font-bold">{destination.name}</div> <div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<div class="truncate text-center">{destination.teams[0].name}</div> <div class="truncate text-center">{destination.teams[0].name}</div>

View File

@ -189,54 +189,52 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<div class="title font-bold">Teams</div> <div class="title font-bold">Teams</div>
<div class="flex items-center justify-center pt-10"> <div class="flex-col items-center justify-center pt-10">
<div class="flex flex-col"> <div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row"> {#each ownTeams as team}
{#each ownTeams as team} <a href="/iam/team/{team.id}" class="p-2 no-underline">
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline"> <div class="box-selection relative">
<div class="box-selection relative"> <div>
<div> <div class="truncate text-center text-xl font-bold">
<div class="truncate text-center text-xl font-bold"> {team.name}
{team.name}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
</div> </div>
<div class="flex items-center justify-center pt-3"> <div class="mt-1 text-center text-xs">
<button {team.permissions?.length} member(s)
on:click|preventDefault={() => switchTeam(team.id)}
class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id}
disabled={$appSession.teamId === team.id}
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
>
</div> </div>
</div> </div>
<div class="flex items-center justify-center pt-3">
<button
on:click|preventDefault={() => switchTeam(team.id)}
class:bg-fuchsia-600={$appSession.teamId !== team.id}
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
class:bg-transparent={$appSession.teamId === team.id}
disabled={$appSession.teamId === team.id}
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
>
</div>
</div>
</a>
{/each}
</div>
{#if $appSession.teamId === '0' && allTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
{#each allTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>
</a> </a>
{/each} {/each}
</div> </div>
{#if $appSession.teamId === '0' && allTeams.length > 0} {/if}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
{#each allTeams as team}
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
</div>
</a>
{/each}
</div>
{/if}
</div>
</div> </div>
</div> </div>

View File

@ -121,10 +121,10 @@
<div class="flex space-x-2 h-8 items-center justify-center pt-8"> <div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button <button
type="submit" type="submit"
class="hover:bg-coollabs-100 text-white" class="text-white"
disabled={loading} disabled={loading}
class:hover:bg-coollabs-100={!loading}
class:bg-transparent={loading} class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading} class:bg-coollabs={!loading}
>{loading ? $t('register.registering') : $t('register.register')}</button >{loading ? $t('register.registering') : $t('register.register')}</button
> >

View File

@ -11,7 +11,7 @@
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@ -30,25 +30,63 @@
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = false; let loading = false;
let loadingVerification = false; let loadingVerification = false;
let dualCerts = service.dualCerts; let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/services/${id}/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
async function handleSubmit() { async function handleSubmit() {
if (loading) return; if (loading) return;
loading = true; loading = true;
try { try {
await post(`/services/${id}/check`, { await post(`/services/${id}/check`, {
fqdn: service.fqdn, fqdn: service.fqdn,
forceSave,
dualCerts,
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [], otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
exposePort: service.exposePort exposePort: service.exposePort
}); });
await post(`/services/${id}`, { ...service }); await post(`/services/${id}`, { ...service });
setLocation(service) setLocation(service);
$disabledButton = false; $disabledButton = false;
forceSave = false;
toast.push('Configuration saved.'); toast.push('Configuration saved.');
} catch (error) { } catch (error) {
//@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(service.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading = false;
@ -102,14 +140,21 @@
<div class="mx-auto max-w-4xl px-6 pb-12"> <div class="mx-auto max-w-4xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5">
<div class="title">{$t('general')}</div> <div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
class:bg-pink-600={!loading} class:bg-pink-600={!loading}
class:bg-orange-600={forceSave}
class:hover:bg-pink-500={!loading} class:hover:bg-pink-500={!loading}
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
> >
{/if} {/if}
{#if service.type === 'plausibleanalytics' && $status.service.isRunning} {#if service.type === 'plausibleanalytics' && $status.service.isRunning}
@ -145,7 +190,7 @@
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label> <label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a <a
href={$appSession.isAdmin && !$status.service.isRunning href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}` ? `/services/${id}/configuration/version?from=/services/${id}`
: ''} : ''}
class="no-underline" class="no-underline"
@ -153,7 +198,8 @@
<input <input
value={service.version} value={service.version}
id="service" id="service"
disabled={$status.service.isRunning} readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning} class:cursor-pointer={!$status.service.isRunning}
/></a /></a
> >
@ -183,8 +229,8 @@
<CopyPasswordField <CopyPasswordField
placeholder="eg: https://console.min.io" placeholder="eg: https://console.min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning} readonly={isDisabled}
disabled={!$appSession.isAdmin || $status.service.isRunning} disabled={isDisabled}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -201,7 +247,7 @@
<CopyPasswordField <CopyPasswordField
placeholder="eg: https://min.io" placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning} readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning} disabled={isDisabled}
name="apiFqdn" name="apiFqdn"
id="apiFqdn" id="apiFqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -221,7 +267,9 @@
<CopyPasswordField <CopyPasswordField
placeholder="eg: https://analytics.coollabs.io" placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning} readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning} disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -230,7 +278,38 @@
/> />
</div> </div>
{/if} {/if}
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
disabled={$status.service.isRunning} disabled={$status.service.isRunning}
@ -245,7 +324,9 @@
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label> <label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input <input
readonly={!$appSession.isAdmin && !$status.service.isRunning} readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning} disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort" name="exposePort"
id="exposePort" id="exposePort"
bind:value={service.exposePort} bind:value={service.exposePort}

View File

@ -76,13 +76,13 @@
<label for="extraConfig">{$t('forms.extra_config')}</label> <label for="extraConfig">{$t('forms.extra_config')}</label>
<textarea <textarea
bind:value={service.wordpress.extraConfig} bind:value={service.wordpress.extraConfig}
disabled={$status.service.isRunning} disabled={$status.service.isRunning || $status.service.initialLoading}
readonly={$status.service.isRunning} readonly={$status.service.isRunning}
class:resize-none={$status.service.isRunning} class:resize-none={$status.service.isRunning}
rows="5" rows="5"
name="extraConfig" name="extraConfig"
id="extraConfig" id="extraConfig"
placeholder={!$status.service.isRunning placeholder={!$status.service.isRunning && !$status.service.initialLoading
? `${$t('forms.eg')}: ? `${$t('forms.eg')}:
define('WP_ALLOW_MULTISITE', true); define('WP_ALLOW_MULTISITE', true);
@ -112,7 +112,14 @@ define('SUBDOMAIN_INSTALL', false);`
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="ftpPassword">Password</label> <label for="ftpPassword">Password</label>
<CopyPasswordField id="ftpPassword" isPasswordField readonly disabled name="ftpPassword" value={ftpPassword} /> <CopyPasswordField
id="ftpPassword"
isPasswordField
readonly
disabled
name="ftpPassword"
value={ftpPassword}
/>
</div> </div>
{/if} {/if}
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">

View File

@ -120,8 +120,9 @@
async function getStatus() { async function getStatus() {
if ($status.service.loading) return; if ($status.service.loading) return;
$status.service.loading = true; $status.service.loading = true;
const data = await get(`/services/${id}`); const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning; $status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited;
$status.service.initialLoading = false; $status.service.initialLoading = false;
$status.service.loading = false; $status.service.loading = false;
} }
@ -173,6 +174,32 @@
> >
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
{/if} {/if}
{#if $status.service.isExited}
<a
href={!$disabledButton ? `/services/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.service.initialLoading} {#if $status.service.initialLoading}
<button <button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
@ -347,7 +374,7 @@
> >
<div class="border border-stone-700 h-8" /> <div class="border border-stone-700 h-8" />
<a <a
href={$status.service.isRunning ? `/services/${id}/logs` : null} href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`} class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}

View File

@ -9,7 +9,7 @@
redirect: `/services/${params.id}` redirect: `/services/${params.id}`
}; };
} }
const response = await get(`/destinations`); const response = await get(`/destinations?onlyVerified=true`);
return { return {
props: { props: {
...response ...response
@ -54,7 +54,7 @@
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/services/${params.id}/logs`);
return {
props: {
service: stuff.service,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts"> <script lang="ts">
export let service: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte'; import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onDestroy, onMount } from 'svelte';
let service: any = {};
let logsLoading = false;
let loadLogsInterval: any = null; let loadLogsInterval: any = null;
let logs: any = []; let logs: any = [];
let lastLog: any = null; let lastLog: any = null;
@ -36,18 +17,23 @@ import { errorNotification } from '$lib/common';
let position = 0; let position = 0;
const { id } = $page.params; const { id } = $page.params;
onMount(async () => { onMount(async () => {
const response = await get(`/services/${id}`);
service = response.service;
loadAllLogs(); loadAllLogs();
loadLogsInterval = setInterval(() => { loadLogsInterval = setInterval(() => {
loadLogs(); loadLogs();
}, 1000); }, 1000);
}); });
onDestroy(() => { onDestroy(() => {
clearInterval(loadLogsInterval); clearInterval(loadLogsInterval);
clearInterval(followingInterval); clearInterval(followingInterval);
}); });
async function loadAllLogs() { async function loadAllLogs() {
try { try {
logsLoading = true;
const data: any = await get(`/services/${id}/logs`); const data: any = await get(`/services/${id}/logs`);
if (data?.logs) { if (data?.logs) {
lastLog = data.logs[data.logs.length - 1]; lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,14 @@ import { errorNotification } from '$lib/common';
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorNotification(error); return errorNotification(error);
} finally {
logsLoading = false;
} }
} }
async function loadLogs() { async function loadLogs() {
if (logsLoading) return;
try { try {
const newLogs: any = await get( const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); logs = logs.concat(newLogs.logs);
@ -137,7 +124,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval} {#if loadLogsInterval}
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
<div class="flex justify-end sticky top-0 p-2 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent" class="bg-transparent"

View File

@ -73,7 +73,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownServices as service} {#each ownServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline"> <a href="/services/{service.id}" class=" p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600"> <div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} /> <Services type={service.type} />
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
@ -85,6 +85,9 @@
{#if service.fqdn} {#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div> <div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if} {/if}
{#if service.destinationDocker?.name}
<div class="truncate text-center">{service.destinationDocker.name}</div>
{/if}
{#if !service.type || !service.fqdn} {#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')} {$t('application.configuration.configuration_missing')}
@ -95,10 +98,10 @@
{/each} {/each}
</div> </div>
{#if otherServices.length > 0 && $appSession.teamId === '0'} {#if otherServices.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Services</div> <div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Services</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherServices as service} {#each otherServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline"> <a href="/services/{service.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600"> <div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} /> <Services type={service.type} />
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { page } from '$app/stores';
import { appSession } from '$lib/store';
</script>
<div class="flex flex-col pt-4 space-y-6 w-96 px-20">
{#if $appSession.teamId === '0'}
<a
href="/settings/global"
class="sub-menu no-underline"
class:sub-menu-active={$page.routeId === 'settings/global'}
>
Global Settings
</a>
{/if}
<a
href="/settings/ssh-keys"
class="sub-menu no-underline"
class:sub-menu-active={$page.routeId === 'settings/ssh-keys'}>SSH Keys</a
>
</div>

View File

@ -0,0 +1,23 @@
<script context="module" lang="ts">
import { get } from '$lib/api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/settings`);
return {
stuff: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<slot />

View File

@ -0,0 +1,302 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let settings: any;
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { del, get, post } from '$lib/api';
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { appSession, features } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
remove: false,
proxyMigration: false
};
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
try {
const { redirect } = await del(`/settings`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch (error) {
return errorNotification(error);
} finally {
loading.remove = false;
}
}
}
async function changeSettings(name: any) {
try {
resetView();
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
await post(`/settings`, {
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled
});
return toast.push(t.get('application.settings_saved'));
} catch (error) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings`, { fqdn });
return window.location.reload();
}
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings`, { minPort, maxPort });
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
toast.push('Configuration saved.');
} catch (error) {
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(settings.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
console.log(error);
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/settings/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
function resetView() {
forceSave = false;
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="flex flex-row">
<Menu />
<div>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-yellow-500={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-yellow-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
</form>
</div>
</div>
</div>

View File

@ -1,357 +1,8 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
try {
const response = await get(`/settings`);
return {
props: {
...response
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts"> <script lang="ts">
export let settings: any; import { goto } from '$app/navigation';
import { appSession } from '$lib/store';
import Setting from '$lib/components/Setting.svelte'; if ($appSession.teamId !== '0') {
import Explainer from '$lib/components/Explainer.svelte'; goto('/settings/ssh-keys');
import { del, get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { appSession, features, isTraefikUsed } from '$lib/store';
import { errorNotification, getDomain } from '$lib/common';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
$isTraefikUsed = settings.isTraefikUsed;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
remove: false,
proxyMigration: false
};
async function removeFqdn() {
if (fqdn) {
loading.remove = true;
try {
const { redirect } = await del(`/settings`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch (error ) {
return errorNotification(error);
} finally {
loading.remove = false;
}
}
}
async function changeSettings(name: any) {
try {
resetView();
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled;
}
await post(`/settings`, {
isRegistrationEnabled,
dualCerts,
isAutoUpdateEnabled,
isDNSCheckEnabled
});
return toast.push(t.get('application.settings_saved'));
} catch (error) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings`, { fqdn });
return window.location.reload();
}
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings`, { minPort, maxPort });
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
} catch (error ) {
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(settings.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
console.log(error)
return errorNotification(error);
} finally {
loading.save = false;
}
}
async function isDNSValid(domain: any, isWWW: any) {
try {
await get(`/settings/check?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch (error) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
function resetView() {
forceSave = false;
}
async function migrateProxy(to: any) {
if (loading.proxyMigration) return;
try {
loading.proxyMigration = true;
await post(`/update`, { type: to });
const data = await get(`/settings`);
$isTraefikUsed = data.settings.isTraefikUsed;
return toast.push('Proxy migration started, it takes a few seconds.');
} catch (error) {
return errorNotification(error);
} finally {
loading.proxyMigration = false;
}
} }
goto('/settings/global');
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
{#if $appSession.teamId === '0'}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-green-600={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
</form>
{#if !settings.isTraefikUsed}
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
</div>
<Explainer
text={$t('setting.credential_stat_explainer', {
link: fqdn
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
: browser &&
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
window.location.hostname +
':8404'
})}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">{$t('forms.password')}</label>
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
</div>
{/if}
</div>
{:else}
<div class="mx-auto max-w-4xl px-6">
<!-- <Language /> -->
</div>
{/if}

View File

@ -0,0 +1,150 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let sshKeys: any;
import { del, post } from '$lib/api';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import Menu from './_Menu.svelte';
let loading = {
save: false
};
let isModalActive = false;
let newSSHKey = {
name: null,
privateKey: null
};
async function handleSubmit() {
try {
await post(`/settings/sshKey`, { ...newSSHKey });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteSSHKey(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?');
if (sure) {
try {
if (!id) return;
await del(`/settings/sshKey`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="flex flex-row">
<Menu />
<div class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">SSH Keys</div>
<button
on:click={() => (isModalActive = true)}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
disabled={loading.save}>New SSH Key</button
>
</div>
<div class="grid grid-flow-col gap-2 px-10">
{#if sshKeys.length === 0}
<div class="text-sm ">No SSH keys found</div>
{:else}
{#each sshKeys as key}
<div class="box-selection group relative">
<div class="text-xl font-bold">{key.name}</div>
<div class="py-3 text-stone-600">Added on {key.createdAt}</div>
<button on:click={() => deleteSSHKey(key.id)} class="bg-red-500">Delete</button>
</div>
{/each}
{/if}
</div>
</div>
</div>
</div>
{#if isModalActive}
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-coolgray-500 bg-opacity-75 transition-opacity" />
<div class="fixed z-10 inset-0 overflow-y-auto text-white">
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
<form
on:submit|preventDefault={handleSubmit}
class="relative bg-coolblack rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6 border border-coolgray-500"
>
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button
on:click={() => (isModalActive = false)}
type="button"
class=" rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span class="sr-only">Close</span>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium pb-4" id="modal-title">New SSH Key</h3>
<div class="text-xs text-stone-400">Add an SSH key to your Coolify instance.</div>
<div class="mt-2">
<label for="privateKey" class="pb-2">Key</label>
<textarea
id="privateKey"
required
bind:value={newSSHKey.privateKey}
class="w-full"
rows={15}
/>
</div>
<div class="mt-2">
<label for="name" class="pb-2">Name</label>
<input id="name" required bind:value={newSSHKey.name} class="w-full" />
</div>
</div>
</div>
<div class="mt-5 flex space-x-4 justify-end">
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
<button on:click={() => (isModalActive = false)} type="button" class="">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{/if}

View File

@ -66,11 +66,7 @@
}); });
const { organization, htmlUrl } = source; const { organization, htmlUrl } = source;
const { fqdn } = settings; const { fqdn } = settings;
const host = dev const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || '';
? getAPIUrl()
: fqdn
? fqdn
: `http://${window.location.host}` || '';
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
let url = 'settings/apps/new'; let url = 'settings/apps/new';
@ -80,9 +76,7 @@
name: `coolify-${name}`, name: `coolify-${name}`,
url: host, url: host,
hook_attributes: { hook_attributes: {
url: dev url: dev ? getWebhookUrl('github') : `${host}/webhooks/github/events`
? getWebhookUrl('github')
: `${host}/webhooks/github/events`
}, },
redirect_url: `${host}/webhooks/github`, redirect_url: `${host}/webhooks/github`,
callback_urls: [`${host}/login/github/app`], callback_urls: [`${host}/login/github/app`],
@ -118,10 +112,10 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
{#if !source.githubAppId} {#if !source.githubAppId}
<form on:submit|preventDefault={newGithubApp} class="py-4"> <form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="flex space-x-1 pb-7 font-bold"> <div class="flex space-x-1 pb-7">
<div class="title">General</div> <div class="title">General</div>
{#if !source.githubAppId} {#if !source.githubAppId}
<button class="bg-orange-600" type="submit">Save</button> <button class="bg-orange-600 font-normal" type="submit">Save</button>
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
@ -173,7 +167,7 @@
</form> </form>
{:else if source.githubApp?.installationId} {:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 ">
<div class="title">{$t('general')}</div> <div class="title">{$t('general')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
@ -218,20 +212,24 @@
bind:value={source.apiUrl} bind:value={source.apiUrl}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if selfHosted}
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label> <div class="grid grid-cols-2 items-center">
<input <label for="customPort" class="text-base font-bold text-stone-100"
name="customPort" >Custom SSH Port</label
id="customPort" >
disabled={!selfHosted} <input
readonly={!selfHosted} name="customPort"
required id="customPort"
value={source.customPort} disabled={!selfHosted}
/> readonly={!selfHosted}
<Explainer required
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions." value={source.customPort}
/> />
</div> <Explainer
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
/>
</div>
{/if}
<div class="grid grid-cols-2"> <div class="grid grid-cols-2">
<div class="flex flex-col"> <div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100" <label for="organization" class="pt-2 text-base font-bold text-stone-100"

View File

@ -31,7 +31,7 @@
appSecret: null appSecret: null
}; };
} }
$: selfHosted = source.htmlUrl !== 'https://gitlab.com' ; $: selfHosted = source.htmlUrl !== 'https://gitlab.com';
onMount(() => { onMount(() => {
oauthIdEl && oauthIdEl.focus(); oauthIdEl && oauthIdEl.focus();
@ -141,7 +141,7 @@
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-7 font-bold"> <div class="flex space-x-1 pb-7">
<div class="title">General</div> <div class="title">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
@ -228,20 +228,22 @@
bind:value={source.apiUrl} bind:value={source.apiUrl}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if selfHosted}
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label> <div class="grid grid-cols-2 items-center">
<input <label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
name="customPort" <input
id="customPort" name="customPort"
disabled={!selfHosted} id="customPort"
readonly={!selfHosted} disabled={!selfHosted}
required readonly={!selfHosted}
bind:value={source.customPort} required
/> bind:value={source.customPort}
<Explainer />
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions." <Explainer
/> text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
</div> />
</div>
{/if}
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="flex-col"> <div class="flex-col">
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100" <label for="oauthId" class="pt-2 text-base font-bold text-stone-100"

View File

@ -66,7 +66,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownSources as source} {#each ownSources as source}
<a href="/sources/{source.id}" class="w-96 p-2 no-underline"> <a href="/sources/{source.id}" class="p-2 no-underline">
<div <div
class="box-selection group relative hover:bg-orange-600" class="box-selection group relative hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}
@ -128,10 +128,10 @@
</div> </div>
{#if otherSources.length > 0 && $appSession.teamId === '0'} {#if otherSources.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div> <div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Sources</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherSources as source} {#each otherSources as source}
<a href="/sources/{source.id}" class="w-96 p-2 no-underline"> <a href="/sources/{source.id}" class="p-2 no-underline">
<div <div
class="box-selection group hover:bg-orange-600" class="box-selection group hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}

View File

@ -92,7 +92,7 @@ label {
} }
button, .button { button, .button {
@apply rounded bg-coolgray-200 p-1 px-2 py-1 text-xs font-bold outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600; @apply rounded bg-coolgray-200 p-2 px-3 text-sm outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600;
} }
a { a {
@ -104,7 +104,7 @@ a {
} }
.title { .title {
@apply mr-4 text-base tracking-tight md:text-2xl; @apply mr-4 text-base tracking-tight md:text-2xl font-bold;
} }
.nav-main { .nav-main {
@apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-auto border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible; @apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-auto border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible;
@ -395,3 +395,11 @@ a {
transform: scale(0.9); transform: scale(0.9);
} }
} }
.sub-menu {
@apply text-xl font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer;
}
.sub-menu-active {
@apply bg-coolgray-500 text-white;
}

View File

@ -1 +0,0 @@
docker buildx build --platform linux/amd64,linux/arm64 -t coollabsio/prisma-engine:3.15 -f prisma-engine.Dockerfile --push .

View File

@ -1,10 +0,0 @@
FROM rust:1.61-alpine3.16 as prisma
WORKDIR /prisma
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc
RUN git clone --depth=1 --branch=3.15.x https://github.com/prisma/prisma-engines.git /prisma
RUN cargo build --release
FROM alpine
WORKDIR /prisma-engines
COPY --from=prisma /prisma/target/release/query-engine /prisma/target/release/migration-engine /prisma/target/release/introspection-engine /prisma/target/release/prisma-fmt /prisma-engines/

View File

@ -1,9 +1,11 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.1.3", "version": "3.2.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "github:coollabsio/coolify",
"scripts": { "scripts": {
"oc": "opencollective-setup",
"db:studio": "pnpm run --filter coolify-api db:studio", "db:studio": "pnpm run --filter coolify-api db:studio",
"db:push": "pnpm run --filter coolify-api db:push", "db:push": "pnpm run --filter coolify-api db:push",
"db:seed": "pnpm run --filter coolify-api db:seed", "db:seed": "pnpm run --filter coolify-api db:seed",
@ -18,13 +20,14 @@
"build": "NODE_ENV=production run-p -n build:*", "build": "NODE_ENV=production run-p -n build:*",
"build:api": "NODE_ENV=production pnpm run --filter coolify-api build", "build:api": "NODE_ENV=production pnpm run --filter coolify-api build",
"build:ui": "NODE_ENV=production pnpm run --filter coolify-ui build", "build:ui": "NODE_ENV=production pnpm run --filter coolify-ui build",
"dockerlogin":"echo $DOCKER_PASS | docker login --username=$DOCKER_USER --password-stdin", "dockerlogin": "echo $DOCKER_PASS | docker login --username=$DOCKER_USER --password-stdin",
"release:staging:amd": "cross-var docker buildx build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .", "release:staging:amd": "cross-var docker buildx build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .",
"release:local":"rm -fr ./local-serve && mkdir ./local-serve && pnpm build && cp -Rp apps/api/build/* ./local-serve && cp -Rp apps/ui/build/ ./local-serve/public && cp -Rp apps/api/prisma/ ./local-serve/prisma && cp -Rp apps/api/package.json ./local-serve && env | grep '^COOLIFY_' > ./local-serve/.env && cd ./local-serve && pnpm install . && pnpm start" "release:local": "rm -fr ./local-serve && mkdir ./local-serve && pnpm build && cp -Rp apps/api/build/* ./local-serve && cp -Rp apps/ui/build/ ./local-serve/public && cp -Rp apps/api/prisma/ ./local-serve/prisma && cp -Rp apps/api/package.json ./local-serve && env | grep '^COOLIFY_' > ./local-serve/.env && cd ./local-serve && pnpm install . && pnpm start"
}, },
"devDependencies": { "devDependencies": {
"cross-var": "1.1.0", "cross-var": "1.1.0",
"npm-run-all": "4.1.5" "npm-run-all": "4.1.5",
"opencollective-setup": "1.4.1"
}, },
"keywords": [ "keywords": [
"docker", "docker",

929
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff