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": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-azuretools.vscode-docker",
"dbaeumer.vscode-eslint",
"svelte.svelte-vscode",
"ardenivanov.svelte-intellisense",

View File

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

View File

@ -9,3 +9,5 @@ tasks:
ports:
- port: 3001
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
RUN apk add --no-cache curl
@ -9,7 +9,7 @@ RUN pnpm install
RUN pnpm build
# Production build
FROM node:18-alpine
FROM node:18-alpine3.16
WORKDIR /app
ENV NODE_ENV production
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 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-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
COPY --from=build /app/apps/api/build/ .

103
README.md
View File

@ -1,6 +1,32 @@
# 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
@ -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!
---
## How to install
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).
## Features
---
ARM support is in beta!
## Features
### 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
- GitLab
- Bitbucket (WIP)
<a href="https://github.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/github.com"></a>
<a href="https://gitlab.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/gitlab.com"></a>
### Destinations
You can deploy your applications to the following destinations:
- Local Docker Engine
- Remote Docker Engine (WIP)
- Kubernetes (WIP)
- Remote Docker Engine
### 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
- NodeJS
- VueJS
- NuxtJS
- NextJS
- React/Preact
- Gatsby
- Svelte
- PHP
- Laravel
- Rust
- Docker
- Python
- Deno
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.
<a href="https://html5.org/">
<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>
<a href="https://nodejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nodejs.org"></a>
<a href="https://vuejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/vuejs.org"></a>
<a href="https://nuxtjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nuxtjs.org"></a>
<a href="https://nextjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nextjs.org"></a>
<a href="https://reactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/reactjs.org"></a>
<a href="https://preactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/preactjs.org"></a>
<a href="https://gatsbyjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/gatsbyjs.org"></a>
<a href="https://svelte.dev"><img style="width:40px;height:40px" src="https://icon.horse/icon/svelte.dev"></a>
<a href="https://php.net"><img style="width:40px;height:40px" src="https://icon.horse/icon/php.net"></a>
<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
One-click database is ready to be used internally or shared over the internet:
- MongoDB
- MariaDB
- MySQL
- PostgreSQL
- CouchDB
- Redis
<a href="https://mongodb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mongodb.org"></a>
<a href="https://mariadb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mariadb.org"></a>
<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>
<a href="https://postgresql.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/postgresql.org"></a>
<a href="https://couchdb.apache.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/couchdb.apache.org"></a>
<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)
- [Ghost](https://ghost.org)
- [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)
- [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
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)
- Discord: [Invitation](https://discord.gg/xhBCC7eGKw)
## Contribute
See [our contribution guide](./CONTRIBUTING.md).
## 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": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.1.0",
"@fastify/cookie": "7.1.0",
"@fastify/cookie": "7.3.1",
"@fastify/cors": "8.0.0",
"@fastify/env": "4.0.0",
"@fastify/jwt": "6.3.1",
"@fastify/static": "6.4.0",
"@fastify/static": "6.4.1",
"@iarna/toml": "2.2.5",
"@prisma/client": "3.15.2",
"axios": "0.27.2",
@ -29,30 +29,33 @@
"cabin": "9.1.2",
"compare-versions": "4.1.3",
"cuid": "2.1.8",
"dayjs": "1.11.3",
"dayjs": "1.11.4",
"dockerode": "3.3.2",
"dotenv-extended": "2.9.0",
"fastify": "4.2.1",
"fastify": "4.3.0",
"fastify-plugin": "4.0.0",
"generate-password": "1.7.0",
"get-port": "6.1.2",
"got": "12.1.0",
"is-ip": "4.0.0",
"got": "12.2.0",
"is-ip": "5.0.0",
"is-port-reachable": "4.0.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"node-forge": "1.3.1",
"node-os-utils": "1.3.7",
"p-queue": "7.2.0",
"public-ip": "6.0.1",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.0.4",
"@types/node": "18.6.1",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"esbuild": "0.14.49",
"eslint": "8.19.0",
"@typescript-eslint/eslint-plugin": "5.31.0",
"@typescript-eslint/parser": "5.31.0",
"esbuild": "0.14.50",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"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)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipv4 String?
ipv6 String?
arch String?
}
model User {
@ -30,39 +33,40 @@ model User {
email String @unique
type String
password String?
teams Team[]
permission Permission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
permission Permission[]
teams Team[]
}
model Permission {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
team Team @relation(fields: [teamId], references: [id])
teamId String
permission String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
}
model Team {
id String @id @default(cuid())
users User[]
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
databaseId String?
serviceId String?
permissions Permission[]
sshKey SshKey[]
applications Application[]
database Database[]
destinationDocker DestinationDocker[]
gitSources GitSource[]
gitHubApps GithubApp[]
gitLabApps GitlabApp[]
destinationDocker DestinationDocker[]
permissions Permission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
database Database[] @relation(references: [id])
databaseId String?
service Service[] @relation(references: [id])
serviceId String?
service Service[]
users User[]
}
model TeamInvitation {
@ -101,21 +105,20 @@ model Application {
denoOptions String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings?
teams Team[]
destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
gitSourceId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[]
persistentStorage ApplicationPersistentStorage[]
baseImage 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 {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique
dualCerts Boolean @default(false)
debug Boolean @default(false)
@ -123,26 +126,27 @@ model ApplicationSettings {
autodeploy Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
}
model ApplicationPersistentStorage {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
@@unique([applicationId, path])
}
model ServicePersistentStorage {
id String @id @default(cuid())
service Service @relation(fields: [serviceId], references: [id])
serviceId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@unique([serviceId, path])
}
@ -155,8 +159,8 @@ model Secret {
isBuildSecret Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])
applicationId String
application Application @relation(fields: [applicationId], references: [id])
@@unique([name, applicationId, isPRMRSecret])
}
@ -167,8 +171,8 @@ model ServiceSecret {
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
serviceId String
service Service @relation(fields: [serviceId], references: [id])
@@unique([name, serviceId])
}
@ -200,21 +204,37 @@ model DestinationDocker {
id String @id @default(cuid())
network String @unique
name String
engine String
engine String?
remoteEngine Boolean @default(false)
remoteIpAddress String?
remoteUser String?
remotePort Int?
remoteVerified Boolean @default(false)
isCoolifyProxyUsed Boolean? @default(false)
teams Team[]
application Application[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sshKeyId String?
sshKey SshKey? @relation(fields: [sshKeyId], references: [id])
application Application[]
database Database[]
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 {
id String @id @default(cuid())
name String
teams Team[]
type String?
apiUrl String?
htmlUrl String?
@ -223,16 +243,16 @@ model GitSource {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
githubAppId String? @unique
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[]
gitlabAppId String? @unique
gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id])
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[]
teams Team[]
}
model GithubApp {
id String @id @default(cuid())
name String? @unique
teams Team[]
appId Int?
installationId Int?
clientId String?
@ -242,13 +262,13 @@ model GithubApp {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
gitSource GitSource?
teams Team[]
}
model GitlabApp {
id String @id @default(cuid())
oauthId Int @unique
groupName String? @unique
teams Team[]
deployKeyId Int?
privateSshKey String?
publicSshKey String?
@ -258,6 +278,7 @@ model GitlabApp {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
gitSource GitSource?
teams Team[]
}
model Database {
@ -271,22 +292,22 @@ model Database {
dbUserPassword String?
rootUser String?
rootUserPassword String?
settings DatabaseSettings?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
destinationDockerId String?
teams Team[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
settings DatabaseSettings?
teams Team[]
}
model DatabaseSettings {
id String @id @default(cuid())
database Database @relation(fields: [databaseId], references: [id])
databaseId String @unique
isPublic Boolean @default(false)
appendOnly Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
database Database @relation(fields: [databaseId], references: [id])
}
model Service {
@ -297,23 +318,23 @@ model Service {
dualCerts Boolean @default(false)
type String?
version String?
teams Team[]
destinationDockerId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plausibleAnalytics PlausibleAnalytics?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
fider Fider?
ghost Ghost?
hasura Hasura?
meiliSearch MeiliSearch?
minio Minio?
moodle Moodle?
plausibleAnalytics PlausibleAnalytics?
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
umami Umami?
vscodeserver Vscodeserver?
wordpress Wordpress?
ghost Ghost?
serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
umami Umami?
hasura Hasura?
fider Fider?
moodle Moodle?
teams Team[]
}
model PlausibleAnalytics {
@ -328,9 +349,9 @@ model PlausibleAnalytics {
secretKeyBase String?
scriptName String @default("plausible.js")
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Minio {
@ -340,18 +361,18 @@ model Minio {
publicPort Int?
apiFqdn String?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Vscodeserver {
id String @id @default(cuid())
password String
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Wordpress {
@ -374,9 +395,9 @@ model Wordpress {
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Ghost {
@ -390,18 +411,18 @@ model Ghost {
mariadbDatabase String?
mariadbPublicPort Int?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model MeiliSearch {
id String @id @default(cuid())
masterKey String
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Umami {
@ -413,9 +434,9 @@ model Umami {
postgresqlPublicPort Int?
umamiAdminPassword String
hashSalt String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Hasura {
@ -426,9 +447,9 @@ model Hasura {
postgresqlDatabase String
postgresqlPublicPort Int?
graphQLAdminPassword String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Fider {
@ -448,9 +469,9 @@ model Fider {
emailSmtpUser String?
emailSmtpPassword String?
emailSmtpEnableStartTls Boolean @default(false)
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model Moodle {
@ -465,7 +486,7 @@ model Moodle {
mariadbRootUserPassword String
mariadbDatabase String
mariadbPublicPort Int?
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

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

View File

@ -5,7 +5,7 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie';
import path, { join } from 'path';
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';
declare module 'fastify' {
@ -101,10 +101,10 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
process.exit(1);
}
console.log(`Coolify's API is listening on ${host}:${port}`);
await initServer()
await initServer();
await scheduler.start('deployApplication');
await scheduler.start('cleanupStorage');
await scheduler.start('checkProxies')
await scheduler.start('checkProxies');
// 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');
}
});
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() {
try {
await asyncExecShell(`docker network create --attachable coolify`);
} 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 { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, asyncExecShell } from '../lib/common';
import { checkContainer, getEngine } from '../lib/docker';
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
(async () => {
if (parentPort) {
// Coolify Proxy
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, 'coolify-haproxy');
const host = getEngine(engine);
const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' });
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
);
await executeDockerCmd({
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) {
const { privatePort } = generateDatabaseConfiguration(database);
// Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${publicPort}`);
const host = getEngine(engine);
const found = await checkContainer({
dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
});
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
);
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
@ -52,12 +54,12 @@ import { checkContainer, getEngine } from '../lib/docker';
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${ftpPublicPort}`);
const host = getEngine(engine);
const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` });
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort} `
);
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
@ -73,12 +75,12 @@ import { checkContainer, getEngine } from '../lib/docker';
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, `${id}-${publicPort}`);
const host = getEngine(engine);
const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort}`
);
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { asyncExecShell, base64Encode, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { scheduler } from "../scheduler";
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { promises as fs } from 'fs';
import { day } from "../dayjs";
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
@ -511,8 +511,8 @@ export async function buildImage({
applicationId,
tag,
workdir,
docker,
buildId,
dockerId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile'
@ -522,6 +522,9 @@ export async function buildImage({
} else {
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) {
await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`,
@ -529,16 +532,61 @@ export async function buildImage({
applicationId
});
}
const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir },
{
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
const { stderr } = await executeDockerCmd({ dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
if (debug) {
const array = stderr.split('\n')
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 }) {
@ -617,18 +665,16 @@ export function makeLabelForStandaloneApplication({
export async function buildCacheImageWithNode(data, imageForBuild) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets,
pullmergeRequestId
} = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
@ -659,11 +705,12 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`);
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) {
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
const { workdir, buildId, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
@ -687,22 +734,16 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`);
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) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
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('RUN cargo chef cook --release --recipe-path recipe.json');
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 { buildImage } from './common';
export default async function ({
applicationId,
debug,
tag,
workdir,
docker,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
}) {
export default async function (data) {
let {
applicationId,
debug,
tag,
workdir,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
} = data
try {
const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
@ -45,7 +45,7 @@ export default async function ({
}
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
await buildImage(data);
} catch (error) {
throw error;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,43 @@
import { asyncExecShell } 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
};
}
import { executeDockerCmd } from './common';
export async function checkContainer(engine: string, container: string, remove = false): Promise<boolean> {
const host = getEngine(engine);
export function formatLabelsOnDocker(data) {
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;
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
);
const { stdout } = await executeDockerCmd({
dockerId,
command:
`docker inspect --format '{{json .State}}' ${container}`
});
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
if (status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
await executeDockerCmd({
dockerId,
command:
`docker rm ${container}`
});
}
if (remove && status === 'exited') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
await executeDockerCmd({
dockerId,
command:
`docker rm ${container}`
});
}
if (isRunning) {
containerFound = true;
@ -38,13 +48,10 @@ export async function checkContainer(engine: string, container: string, remove =
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;
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
if (stdout.trim() === 'exited') {
isExited = true;
}
@ -57,19 +64,17 @@ export async function isContainerExited(engine: string, containerName: string):
export async function removeContainer({
id,
engine
dockerId
}: {
id: string;
engine: string;
dockerId: string;
}): Promise<void> {
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${id}`);
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${id}`);
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
console.log(error);

View File

@ -5,12 +5,12 @@ import axios from 'axios';
import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs';
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 { checkContainer, dockerInstance, getEngine, isContainerExited, removeContainer } from '../../../../lib/docker';
import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler';
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';
export async function listApplications(request: FastifyRequest) {
@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) {
const { teamId } = request.user
const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }
include: { teams: true, destinationDocker: true }
});
const settings = await prisma.setting.findFirst()
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 }) {
return errorHandler({ status, message })
}
@ -68,17 +89,9 @@ export async function getApplication(request: FastifyRequest<OnlyId>) {
const { id } = request.params
const { teamId } = request.user
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
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 {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
application,
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) {
try {
const { id } = request.params
const { teamId } = request.user
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
const { engine } = application.destinationDocker;
const found = await checkContainer(engine, id);
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: application.destinationDocker.id });
}
}
return reply.code(201).send();
@ -304,17 +336,17 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
where: { id },
include: { destinationDocker: true }
});
if (application?.destinationDockerId && application.destinationDocker?.engine && application.destinationDocker?.network) {
const host = getEngine(application.destinationDocker.engine);
const { stdout: containers } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
);
if (application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
})
if (containers) {
const containersArray = containers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
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 })
}
}
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>) {
try {
const { id } = request.params
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 found = await isDomainConfigured({ id, fqdn });
const found = await isDomainConfigured({ id, fqdn, dockerId });
if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (exposePort) {
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 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 (publicPort !== exposePort) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
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 {}
} catch ({ status, message }) {
@ -375,7 +422,7 @@ export async function getUsage(request) {
const application: any = await getApplicationFromDB(id, teamId);
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]);
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]);
}
return {
usage
@ -701,21 +748,20 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
secret.value = decrypt(secret.value);
return secret;
});
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const destinationDocker = await prisma.destinationDocker.findFirst({
where: { application: { some: { id } }, teams: { some: { id: teamId } } }
});
const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({
filters: { network: [destinationDocker.network], name: [id] }
});
const containers = listContainers.filter((container) => {
return (
container.Labels['coolify.configuration'] &&
container.Labels['coolify.type'] === 'standalone-application'
);
});
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
if (stdout === '') {
return {
containers: [],
applicationSecrets: [],
PRMRSecrets: []
}
}
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
@ -733,50 +779,46 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
})
}
} catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message })
}
}
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try {
const { id } = request.params
const { id } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
return { logs: sortedLogs }
// }
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
return {
logs
logs: []
};
}
} catch (error) {
return {
logs: []
};
}
}
return {
message: 'No logs found.'
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}

View File

@ -1,8 +1,8 @@
import { FastifyPluginAsync } from 'fastify';
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> => {
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.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<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(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.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 {
Querystring: { domain: string; };
}
export interface CheckDomain extends OnlyId {
Querystring: { domain: string; };
}
export interface CheckDNS extends OnlyId {
Querystring: { domain: string; };
Body: {
@ -115,3 +118,9 @@ export interface DeployApplication extends OnlyId {
branch: string
}
}
export interface StopPreviewApplication extends OnlyId {
Body: {
pullmergeRequestId: string | null,
}
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { 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.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.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));

View File

@ -1,14 +1,19 @@
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
import sshConfig from 'ssh-config'
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 { 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 {
const teamId = request.user.teamId;
const { onlyVerified = false } = request.query
let destinations = []
if (teamId === '0') {
destinations = await prisma.destinationDocker.findMany({ include: { teams: true } });
@ -18,6 +23,9 @@ export async function listDestinations(request: FastifyRequest) {
include: { teams: true }
});
}
if (onlyVerified) {
destinations = destinations.filter(destination => destination.engine || (destination.remoteEngine && destination.remoteVerified))
}
return {
destinations
}
@ -44,7 +52,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const { id } = request.params
const teamId = request.user?.teamId;
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') {
throw { status: 404, message: `Destination not found.` };
@ -52,23 +61,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
const settings = await listSettings();
let payload = {
destination,
settings,
state: false
settings
};
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 {
...payload
};
@ -79,68 +73,68 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
}
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed } = request.body
const teamId = request.user.teamId;
if (id === 'new') {
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);
const { id } = request.params
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
if (id === 'new') {
console.log(engine)
if (engine) {
const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
if (stdout === '') {
await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`);
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
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) {
const proxyConfigured = destinations.find(
(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 {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
}
} catch ({ status, message }) {
console.log({ status, message })
return errorHandler({ status, message })
}
}
export async function deleteDestination(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine);
const { network } = destination;
const settings = await prisma.setting.findFirst();
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy';
const { stdout: found } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'`
);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
);
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } });
if (isCoolifyProxyUsed) {
if (engine || remoteVerified) {
const { stdout: found } = await executeDockerCmd({
dockerId: id,
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
})
if (found) {
await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` })
}
}
}
await prisma.destinationDocker.delete({ where: { id } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -163,34 +157,105 @@ export async function saveDestinationSettings(request: FastifyRequest<SaveDestin
}
}
export async function startProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body;
const { id } = request.params
try {
await startTraefikProxy(engine);
await startTraefikProxy(id);
return {}
} catch ({ status, message }) {
await stopTraefikProxy(engine);
console.log({ status, message })
await stopTraefikProxy(id);
return errorHandler({ status, message })
}
}
export async function stopProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body;
const { id } = request.params
try {
await stopTraefikProxy(engine);
await stopTraefikProxy(id);
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function restartProxy(request: FastifyRequest<Proxy>) {
const { engine } = request.body;
const { id } = request.params
try {
await stopTraefikProxy(engine);
await startTraefikProxy(engine);
await prisma.destinationDocker.updateMany({
where: { engine },
await stopTraefikProxy(id);
await startTraefikProxy(id);
await prisma.destinationDocker.update({
where: { id },
data: { isCoolifyProxyUsed: true }
});
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 }) {
return errorHandler({ status, message })
}

View File

@ -1,24 +1,29 @@
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 { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types';
import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
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.get<OnlyId>('/:id', async (request) => await getDestination(request));
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
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<Proxy>('/:id/start', async (request, reply) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request, reply) => await stopProxy(request));
fastify.post<Proxy>('/:id/restart', async (request, reply) => await restartProxy(request));
fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));
fastify.post<Proxy>('/:id/start', async (request,) => await startProxy(request));
fastify.post<Proxy>('/:id/stop', async (request) => await stopProxy(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;

View File

@ -1,5 +1,10 @@
import { OnlyId } from "../../../../types"
export interface ListDestinations {
Querystring: {
onlyVerified: string
}
}
export interface CheckDestination {
Body: {
network: string
@ -20,7 +25,5 @@ export interface SaveDestinationSettings 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() {
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -154,7 +155,6 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
}
if (userFound) {
if (userFound.type === 'email') {
// TODO: Review this one
if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password);
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 userFound = await prisma.user.findUnique({ where: { email } });
if (!userFound) {
throw `No user found with '${email}' email address.`
throw {
message: `No user found with '${email}' email address.`
};
}
const uid = userFound.id;
if (uid === userId) {
throw `Invitation to yourself? Whaaaaat?`
if (uid === userId) {
throw {
message: `Invitation to yourself? Whaaaaat?`
};
}
const alreadyInTeam = await prisma.team.findFirst({
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 yaml from 'js-yaml';
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 { checkContainer, dockerInstance, getEngine, removeContainer } from '../../../../lib/docker';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid';
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>) {
// try {
@ -145,15 +145,10 @@ import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteSer
export async function listServices(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let services = []
if (teamId === '0') {
services = await prisma.service.findMany({ include: { teams: true } });
} else {
services = await prisma.service.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true, destinationDocker: true }
});
return {
services
}
@ -172,43 +167,41 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
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>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const service = await getServiceFromDB({ id, teamId });
const settings = await listSettings()
if (!service) {
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 {
isRunning,
service,
settings
}
@ -282,7 +275,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
const service = await getServiceFromDB({ id, teamId });
if (service.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]);
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.id, id)]);
}
return {
usage
@ -299,33 +292,22 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.service.findUnique({
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
return { logs: sortedLogs }
// }
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
@ -364,40 +346,57 @@ export async function saveServiceSettings(request: FastifyRequest<SaveServiceSet
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>) {
try {
const { id } = request.params;
let { fqdn, exposePort, otherFqdns } = request.body;
let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
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) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (otherFqdns && otherFqdns.length > 0) {
for (const ofqdn of otherFqdns) {
found = await isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true });
found = await isDomainConfigured({ id, fqdn: ofqdn, dockerId });
if (found) {
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
}
}
}
if (exposePort) {
const { default: getPort } = await import('get-port');
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
const publicPort = await getPort({ port: exposePort });
if (publicPort !== exposePort) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
if (configuredPort !== exposePort) {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -742,7 +741,6 @@ async function startPlausibleAnalyticsService(request: FastifyRequest<ServiceSta
});
}
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('plausibleanalytics');
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`;
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 --build -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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -877,17 +873,17 @@ async function stopPlausibleAnalyticsService(request: FastifyRequest<ServiceStar
if (destinationDockerId) {
const engine = destinationDocker.engine;
let found = await checkContainer(engine, id);
let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
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) {
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) {
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 } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('nocodb');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -956,8 +951,8 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -970,10 +965,9 @@ async function stopNocodbService(request: FastifyRequest<ServiceStartStop>) {
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
}
return {}
@ -999,10 +993,10 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
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 { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1058,8 +1052,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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` })
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
return {}
} catch ({ status, message }) {
@ -1071,13 +1065,12 @@ async function stopMinioService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.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 } })
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
}
return {}
@ -1103,7 +1096,6 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vscodeserver');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1175,16 +1167,16 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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` })
const changePermissionOn = persistentStorage.map((p) => p.path);
if (changePermissionOn.length > 0) {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
await executeDockerCmd({
dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' '
)}`
);
})
}
return {}
} catch ({ status, message }) {
@ -1196,12 +1188,11 @@ async function stopVscodeService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
}
return {}
@ -1236,7 +1227,6 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const image = getServiceImage(type);
const port = getServiceMainPort('wordpress');
@ -1328,8 +1318,10 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
}
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1346,28 +1338,27 @@ async function stopWordpressService(request: FastifyRequest<ServiceStartStop>) {
wordpress: { ftpEnabled }
} = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-mysql`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mysql` });
if (found) {
await removeContainer({ id: `${id}-mysql`, engine });
await removeContainer({ id: `${id}-mysql`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
if (ftpEnabled) {
const found = await checkContainer(engine, `${id}-ftp`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (found) {
await removeContainer({ id: `${id}-ftp`, engine });
await removeContainer({ id: `${id}-ftp`, dockerId: destinationDocker.id });
}
await prisma.wordpress.update({
where: { serviceId: id },
@ -1393,7 +1384,6 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vaultwarden');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1444,8 +1434,10 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1456,14 +1448,12 @@ async function stopVaultwardenService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -1483,7 +1473,6 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('languagetool');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1536,8 +1525,9 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1548,14 +1538,12 @@ async function stopLanguageToolService(request: FastifyRequest<ServiceStartStop>
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -1575,7 +1563,6 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('n8n');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1628,8 +1615,10 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1640,14 +1629,12 @@ async function stopN8nService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -1667,7 +1654,6 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('uptimekuma');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1719,8 +1705,9 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1731,14 +1718,12 @@ async function stopUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -1774,7 +1759,6 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -1871,8 +1855,9 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1883,18 +1868,16 @@ async function stopGhostService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
let found = await checkContainer(engine, id);
let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
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) {
await removeContainer({ id: `${id}-mariadb`, engine });
await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -1917,7 +1900,6 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('meilisearch');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1972,8 +1954,10 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1984,14 +1968,12 @@ async function stopMeilisearchService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -2024,7 +2006,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('umami');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2191,8 +2172,10 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -2203,22 +2186,20 @@ async function stopUmamiService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) {
await removeContainer({ id: `${id}-postgresql`, engine });
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -2245,7 +2226,6 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('hasura');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2327,8 +2307,9 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -2339,22 +2320,20 @@ async function stopHasuraService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) {
await removeContainer({ id: `${id}-postgresql`, engine });
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -2396,7 +2375,6 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('fider');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2489,8 +2467,8 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
@ -2502,22 +2480,20 @@ async function stopFiderService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) {
await removeContainer({ id: `${id}-postgresql`, engine });
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -2554,12 +2530,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('moodle');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = {
moodle: {
image: `${image}:${version}`,
@ -2652,8 +2626,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
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 {}
} catch ({ status, message }) {
@ -2665,22 +2641,20 @@ async function stopMoodleService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-mariadb`);
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` });
if (found) {
await removeContainer({ id: `${id}-mariadb`, engine });
await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
@ -2703,14 +2677,10 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
const container = await docker.engine.getContainer(id);
const command = await container.exec({
Cmd: [
`psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
]
});
await command.start();
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could not activate users.' }
@ -2722,7 +2692,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
const { id } = request.params
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 ftpPassword = generatePassword();
@ -2742,7 +2715,6 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
ftpHostKeyPrivate
} = data;
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
@ -2789,7 +2761,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
});
try {
const isRunning = await checkContainer(engine, `${id}-ftp`);
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
if (isRunning) {
await asyncExecShell(
`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 fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
);
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
})
}
return reply.code(201).send({
publicPort,
@ -2856,9 +2830,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
data: { ftpPublicPort: null }
});
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
})
} catch (error) {
//
}

View File

@ -3,12 +3,14 @@ import {
activatePlausibleUsers,
activateWordpressFtp,
checkService,
checkServiceDomain,
deleteService,
deleteServiceSecret,
deleteServiceStorage,
getService,
getServiceLogs,
getServiceSecrets,
getServiceStatus,
getServiceStorages,
getServiceType,
getServiceUsage,
@ -28,7 +30,7 @@ import {
} from './handlers';
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> => {
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.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<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));

View File

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

View File

@ -1,15 +1,24 @@
import { promises as dns } from 'dns';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
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 {
settings
settings,
sshKeys: unencryptedKeys
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -68,7 +77,8 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
throw "Domain already configured";
}
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 {};
} catch ({ status, message }) {
@ -83,4 +93,31 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
} catch ({ 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 { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
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.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;

View File

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

View File

@ -8,12 +8,22 @@ export interface GitHubEvents {
Body: {
number: string,
action: string,
repository: string,
repository: {
id: string,
},
ref: string,
pull_request: {
base: {
ref: string,
repo: {
id: string,
}
},
head: {
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}`)
} catch ({ status, message, ...other }) {
console.log(other)
return errorHandler({ status, message })
}
}
@ -117,8 +116,10 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (applicationFound.settings.previews) {
if (applicationFound.destinationDockerId) {
const isRunning = await checkContainer(
applicationFound.destinationDocker.engine,
applicationFound.id
{
dockerId: applicationFound.destinationDocker.id,
container: applicationFound.id
}
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
@ -164,7 +165,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine;
await removeContainer({ id, engine });
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
}
return {
message: 'Removed preview. Thank you!'

View File

@ -1,6 +1,5 @@
import { FastifyRequest } from "fastify";
import { asyncExecShell, errorHandler, getDomain, isDev, listServicesWithIncludes, prisma, supportedServiceTypesAndVersions } from "../../../lib/common";
import { getEngine } from "../../../lib/docker";
import { errorHandler, getDomain, isDev, prisma, supportedServiceTypesAndVersions, include, executeDockerCmd } from "../../../lib/common";
import { TraefikOtherConfiguration } from "./types";
function configureMiddleware(
@ -167,6 +166,7 @@ export async function traefikConfiguration(request, reply) {
}
};
const applications = await prisma.application.findMany({
where: { destinationDocker: { remoteEngine: false } },
include: { destinationDocker: true, settings: true }
});
const data = {
@ -184,7 +184,7 @@ export async function traefikConfiguration(request, reply) {
settings: { previews, dualCerts }
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const { network, id: dockerId } = destinationDocker;
const isRunning = true;
if (fqdn) {
const domain = getDomain(fqdn);
@ -205,10 +205,7 @@ export async function traefikConfiguration(request, reply) {
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
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')
@ -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) {
const {
@ -248,7 +249,6 @@ export async function traefikConfiguration(request, reply) {
plausibleAnalytics
} = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
@ -487,4 +487,219 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
console.log(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 { traefikConfiguration, traefikOtherConfiguration } from './handlers';
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
import { TraefikOtherConfiguration } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request));
};
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=. ."
},
"devDependencies": {
"@playwright/test": "1.23.3",
"@sveltejs/kit": "1.0.0-next.375",
"@playwright/test": "1.23.4",
"@sveltejs/kit": "1.0.0-next.377",
"@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.30.6",
"@typescript-eslint/parser": "5.30.6",
"autoprefixer": "10.4.7",
"eslint": "8.19.0",
"eslint": "8.20.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0",
"postcss": "8.4.14",
@ -34,11 +34,11 @@
"tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4",
"vite": "^3.0.0"
"vite": "3.0.1"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.36",
"@sveltejs/adapter-static": "1.0.0-next.37",
"@zerodevx/svelte-toast": "0.7.2",
"cuid": "2.1.8",
"js-cookie": "3.0.1",

View File

@ -250,7 +250,7 @@
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"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.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"

View File

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

View File

@ -65,11 +65,12 @@
<script lang="ts">
export let baseSettings: any;
$appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6;
$appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
export let settings: any;
export let userId: string;
export let teamId: string;
export let permission: string;
@ -84,15 +85,13 @@
import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
import PageLoader from '$lib/components/PageLoader.svelte';
import { errorNotification } from '$lib/common';
import { appSession, isTraefikUsed } from '$lib/store';
import { appSession } from '$lib/store';
if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId;
if (permission) $appSession.permission = permission;
if (isAdmin) $appSession.isAdmin = isAdmin;
$isTraefikUsed = settings?.isTraefikUsed || false;
async function logout() {
try {
Cookies.remove('token');
@ -322,33 +321,31 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $appSession.teamId === '0'}
<a
sveltekit:prefetch
href="/settings"
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings"
<a
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/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
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"
>
<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}
<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>
<div
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"

View File

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

View File

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

View File

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

View File

@ -106,7 +106,7 @@
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button
on:click={followBuild}
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">
export let application: any;
import { page } from '$app/stores';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
@ -37,6 +18,8 @@
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@ -48,6 +31,7 @@
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/applications/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,9 +40,12 @@
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
@ -157,7 +144,7 @@
{#if loadLogsInterval}
<LoadingLogs />
{/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
on:click={followBuild}
class="bg-transparent"

View File

@ -1,12 +1,10 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
export const load: Load = async ({ stuff, url }) => {
try {
const response = await get(`/applications/${params.id}/previews`);
return {
props: {
application: stuff.application,
...response
application: stuff.application
}
};
} catch (error) {
@ -19,10 +17,7 @@
</script>
<script lang="ts">
export let containers: any;
export let application: any;
export let PRMRSecrets: any;
export let applicationSecrets: any;
import Secret from './_Secret.svelte';
import { get, post } from '$lib/api';
import { page } from '$app/stores';
@ -31,12 +26,33 @@
import { t } from '$lib/translations';
import { goto } from '$app/navigation';
import { errorNotification, getDomain } from '$lib/common';
import { onMount } from 'svelte';
import Loading from '$lib/components/Loading.svelte';
const { id } = $page.params;
let containers: any;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = {
init: true,
removing: false
};
async function refreshSecrets() {
const data = await get(`/applications/${id}/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) {
try {
const { buildId } = await post(`/applications/${id}/deploy`, {
@ -55,6 +71,19 @@
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>
<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">
Preview Deployments
</div>
<span class="text-xs">{application.name} </span>
<span class="text-xs">{application?.name}</span>
</div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
@ -108,68 +137,81 @@
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "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."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</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-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 loading.init}
<Loading />
{:else}
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "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."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</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 flex-wrap justify-center px-2 md:flex-row">
{#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">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
@ -140,15 +140,18 @@
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div>
{/if}
{#if !application.gitSourceId || !application.repository || !application.branch}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Git Source Missing
</div>
{:else if !application.destinationDockerId}
{:else if !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing
</div>
{:else if !application.fqdn}
{:else if !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing
</div>
@ -158,10 +161,10 @@
{/each}
</div>
{#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">
{#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">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}

View File

@ -1,7 +1,6 @@
<script lang="ts">
export let database: any;
export let privatePort: any;
export let settings: any;
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@ -17,8 +16,9 @@
import { post } from '$lib/api';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
import { errorNotification, getDomain } from '$lib/common';
import { errorNotification } from '$lib/common';
import { appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
@ -49,12 +49,15 @@
databaseDbUser = '';
}
}
function generateUrl(): string {
return `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : ''
}${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}`;
}
@ -103,7 +106,7 @@
<div class="mx-auto max-w-4xl px-6">
<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>
{#if $appSession.isAdmin}
<button
@ -153,7 +156,8 @@
>
<input
value={database.version}
disabled={$status.database.isRunning}
readonly
disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning}
/></a
>
@ -203,9 +207,16 @@
<EdgeDB {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
<div>
<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
textarea={true}
placeholder={$t('forms.generated_automatically_after_start')}

View File

@ -58,7 +58,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
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 Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte';
@ -66,6 +66,9 @@
let loading = false;
let statusInterval: any = false;
$disabledButton = !$appSession.isAdmin;
async function deleteDatabase() {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) {
@ -104,7 +107,7 @@
async function getStatus() {
if ($status.database.loading) return;
$status.database.loading = true;
const data = await get(`/databases/${id}`);
const data = await get(`/databases/${id}/status`);
$status.database.isRunning = data.isRunning;
$status.database.initialLoading = false;
$status.database.loading = false;
@ -138,6 +141,32 @@
<Loading fullscreen cover />
{:else}
{#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}
<button
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}`
};
}
const response = await get(`/destinations`);
const response = await get(`/destinations?onlyVerified=true`);
return {
props: {
...response
@ -55,7 +55,7 @@
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<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">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@ -15,7 +15,6 @@
import Databases from './_Databases/_Databases.svelte';
import { status } from '$lib/store';
export let database: any;
export let settings: any;
export let privatePort: any;
const { id } = $page.params;
@ -86,4 +85,4 @@
</dl>
</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">
export let database: any;
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
let loadLogsInterval: any = null;
let logs: any = [];
@ -34,9 +16,12 @@ import { errorNotification } from '$lib/common';
let followingLogs: any;
let logsEl: any;
let position = 0;
let loadingLogs = false;
let database: any = {};
const { id } = $page.params;
onMount(async () => {
const { logs: firstLogs } = await get(`/databases/${id}/logs`);
logs = firstLogs;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@ -48,6 +33,7 @@ import { errorNotification } from '$lib/common';
});
async function loadAllLogs() {
try {
loadingLogs = true;
const data: any = await get(`/databases/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,15 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
async function loadLogs() {
if (loadingLogs) return;
try {
const newLogs: any = await get(
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
loadingLogs = true;
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]) {
logs = logs.concat(newLogs.logs);
@ -70,6 +58,8 @@ import { errorNotification } from '$lib/common';
}
} catch (error) {
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
function detect() {
@ -137,7 +127,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval}
<LoadingLogs />
{/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
on:click={followBuild}
class="bg-transparent"

View File

@ -78,7 +78,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#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">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />
@ -103,6 +103,9 @@
{#if $appSession.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if database.destinationDocker?.name}
<div class="truncate text-center">{database.destinationDocker.name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
@ -113,10 +116,10 @@
{/each}
</div>
{#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">
{#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">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />

View File

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

View File

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

View File

@ -5,23 +5,35 @@
import { post } from '$lib/api';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations';
let loading = false;
async function handleSubmit() {
if (loading) return;
try {
const { id } = await post('/new/destination/docker', {
loading = true;
await post(`/destinations/check`, { network: payload.network });
const { id } = await post(`/destinations/new`, {
...payload
});
return await goto(`/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
}
}
</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">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5">
@ -44,40 +56,36 @@
</div>
<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
>
<input
required
name="ipAddress"
name="remoteIpAddress"
placeholder="{$t('forms.eg')}: 192.168..."
bind:value={payload.ipAddress}
bind:value={payload.remoteIpAddress}
/>
</div>
<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>
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
</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"
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input
required
name="sshPrivateKey"
placeholder="{$t('forms.eg')}: -----BEGIN...."
bind:value={payload.sshPrivateKey}
name="remoteUser"
placeholder="{$t('forms.eg')}: root"
bind:value={payload.remoteUser}
/>
</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">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input

View File

@ -1,80 +1,90 @@
<script lang="ts">
export let destination: any;
export let settings: any
export let state: any
export let settings: any;
import { toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores';
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 { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification, generateRemoteEngine } from '$lib/common';
import { appSession } from '$lib/store';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false;
let restarting = false;
let loading = {
restart: false,
proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading = true;
loading.save = true;
try {
return await post(`/destinations/${id}.json`, { ...destination });
} catch (error ) {
await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error);
} 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 () => {
if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error ) {
return errorNotification(error);
}
} else if (state === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch ( error ) {
return errorNotification(error);
loading.proxy = true;
if (destination.remoteEngine && destination.remoteVerified) {
const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch (error) {
return errorNotification(error);
}
}
}
loading.proxy = false;
});
async function changeProxySetting() {
loading.proxy = true;
if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
const sure = confirm(
`Are you sure you want to ${
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
destination.engine
}'! Nothing will be reachable if you do it!`
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! 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 {
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: proxyUsed,
engine: destination.engine
});
if (isProxyActivated) {
@ -82,15 +92,17 @@ import { appSession } from '$lib/store';
} else {
await startProxy();
}
destination.isCoolifyProxyUsed = proxyUsed;
} catch (error) {
return errorNotification(error);
} finally {
loading.proxy = false;
}
}
}
async function stopProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/stop.json`, { engine });
await post(`/destinations/${id}/stop`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_stopped'));
} catch (error) {
return errorNotification(error);
@ -98,8 +110,7 @@ import { appSession } from '$lib/store';
}
async function startProxy() {
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/start.json`, { engine });
await post(`/destinations/${id}/start`, { engine: destination.engine });
return toast.push($t('destination.coolify_proxy_started'));
} catch (error) {
return errorNotification(error);
@ -109,9 +120,9 @@ import { appSession } from '$lib/store';
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
fqdn: settings.fqdn
});
@ -119,9 +130,24 @@ import { appSession } from '$lib/store';
setTimeout(() => {
window.location.reload();
}, 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>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
@ -131,23 +157,28 @@ import { appSession } from '$lib/store';
<button
type="submit"
class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading.save}
disabled={loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{#if !destination.remoteVerified}
<button
disabled={loading.verify}
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{loading.verify ? 'Verifying...' : 'Verify Remote Docker Engine'}</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}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button
> -->
</div>
<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>
@ -159,22 +190,6 @@ import { appSession } from '$lib/store';
bind:value={destination.name}
/>
</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">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
@ -186,9 +201,53 @@ import { appSession } from '$lib/store';
value={destination.network}
/>
</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">
<Setting
disabled={cannotDisable}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')}
@ -200,27 +259,3 @@ import { appSession } from '$lib/store';
/>
</div>
</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">
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 {
const { id } = params;
const response = await get(`/destinations/${id}`);
const { destination, settings, state } = response;
const { destination, settings } = response;
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
return {
status: 302,
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 {
props: {
destination
},
stuff: {
destination,
settings,
state
settings
}
};
} catch (error) {
console.log(error)
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="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#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
class="w-6"
xmlns="http://www.w3.org/2000/svg"
@ -66,24 +66,112 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
<div class="box-selection hover:bg-sky-600">
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
<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>
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<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>
</a>
{/each}
</div>
{#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">
{#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">
<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>
{#if $appSession.teamId === '0'}
<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="title font-bold">Teams</div>
<div class="flex 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">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</div>
<div class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</div>
<div class="flex-col items-center justify-center pt-10">
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team}
<a href="/iam/team/{team.id}" class="p-2 no-underline">
<div class="box-selection relative">
<div>
<div class="truncate text-center text-xl font-bold">
{team.name}
</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 class="mt-1 text-center text-xs">
{team.permissions?.length} member(s)
</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>
{/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="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>
{/if}
</div>
</div>

View File

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

View File

@ -11,7 +11,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common';
import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
@ -30,25 +30,63 @@
import Moodle from './_Moodle.svelte';
const { id } = $page.params;
$: isDisabled =
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
let forceSave = false;
let loading = false;
let loadingVerification = false;
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() {
if (loading) return;
loading = true;
try {
await post(`/services/${id}/check`, {
fqdn: service.fqdn,
forceSave,
dualCerts,
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
exposePort: service.exposePort
});
await post(`/services/${id}`, { ...service });
setLocation(service)
setLocation(service);
$disabledButton = false;
forceSave = false;
toast.push('Configuration saved.');
} 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);
} finally {
loading = false;
@ -102,14 +140,21 @@
<div class="mx-auto max-w-4xl px-6 pb-12">
<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>
{#if $appSession.isAdmin}
<button
type="submit"
class:bg-pink-600={!loading}
class:bg-orange-600={forceSave}
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 service.type === 'plausibleanalytics' && $status.service.isRunning}
@ -145,7 +190,7 @@
<div class="grid grid-cols-2 items-center px-10">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.service.isRunning
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
@ -153,7 +198,8 @@
<input
value={service.version}
id="service"
disabled={$status.service.isRunning}
readonly
disabled={$status.service.isRunning || $status.service.initialLoading}
class:cursor-pointer={!$status.service.isRunning}
/></a
>
@ -183,8 +229,8 @@
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
readonly={isDisabled}
disabled={isDisabled}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -201,7 +247,7 @@
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
disabled={isDisabled}
name="apiFqdn"
id="apiFqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -221,7 +267,9 @@
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
@ -230,7 +278,38 @@
/>
</div>
{/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">
<Setting
disabled={$status.service.isRunning}
@ -245,7 +324,9 @@
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input
readonly={!$appSession.isAdmin && !$status.service.isRunning}
disabled={!$appSession.isAdmin || $status.service.isRunning}
disabled={!$appSession.isAdmin ||
$status.service.isRunning ||
$status.service.initialLoading}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}

View File

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

View File

@ -120,8 +120,9 @@
async function getStatus() {
if ($status.service.loading) return;
$status.service.loading = true;
const data = await get(`/services/${id}`);
const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited;
$status.service.initialLoading = false;
$status.service.loading = false;
}
@ -173,6 +174,32 @@
>
<div class="border border-stone-700 h-8" />
{/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}
<button
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" />
<a
href={$status.service.isRunning ? `/services/${id}/logs` : null}
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}

View File

@ -9,7 +9,7 @@
redirect: `/services/${params.id}`
};
}
const response = await get(`/destinations`);
const response = await get(`/destinations?onlyVerified=true`);
return {
props: {
...response
@ -54,7 +54,7 @@
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<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">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<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">
export let service: any;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
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 logs: any = [];
let lastLog: any = null;
@ -36,18 +17,23 @@ import { errorNotification } from '$lib/common';
let position = 0;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/services/${id}`);
service = response.service;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/services/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,14 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
@ -137,7 +124,7 @@ import { errorNotification } from '$lib/common';
{#if loadLogsInterval}
<LoadingLogs />
{/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
on:click={followBuild}
class="bg-transparent"

View File

@ -73,7 +73,7 @@
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#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">
<Services type={service.type} />
<div class="truncate text-center text-xl font-bold">
@ -85,6 +85,9 @@
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if service.destinationDocker?.name}
<div class="truncate text-center">{service.destinationDocker.name}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
{$t('application.configuration.configuration_missing')}
@ -95,10 +98,10 @@
{/each}
</div>
{#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">
{#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">
<Services type={service.type} />
<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">
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 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;
}
import { goto } from '$app/navigation';
import { appSession } from '$lib/store';
if ($appSession.teamId !== '0') {
goto('/settings/ssh-keys');
}
goto('/settings/global');
</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 { fqdn } = settings;
const host = dev
? getAPIUrl()
: fqdn
? fqdn
: `http://${window.location.host}` || '';
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || '';
const domain = getDomain(fqdn);
let url = 'settings/apps/new';
@ -80,9 +76,7 @@
name: `coolify-${name}`,
url: host,
hook_attributes: {
url: dev
? getWebhookUrl('github')
: `${host}/webhooks/github/events`
url: dev ? getWebhookUrl('github') : `${host}/webhooks/github/events`
},
redirect_url: `${host}/webhooks/github`,
callback_urls: [`${host}/login/github/app`],
@ -118,10 +112,10 @@
<div class="mx-auto max-w-4xl px-6">
{#if !source.githubAppId}
<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>
{#if !source.githubAppId}
<button class="bg-orange-600" type="submit">Save</button>
<button class="bg-orange-600 font-normal" type="submit">Save</button>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
@ -173,7 +167,7 @@
</form>
{:else if source.githubApp?.installationId}
<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>
{#if $appSession.isAdmin}
<button
@ -218,20 +212,24 @@
bind:value={source.apiUrl}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
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."
/>
</div>
{#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port</label
>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
required
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."
/>
</div>
{/if}
<div class="grid grid-cols-2">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"

View File

@ -31,7 +31,7 @@
appSecret: null
};
}
$: selfHosted = source.htmlUrl !== 'https://gitlab.com' ;
$: selfHosted = source.htmlUrl !== 'https://gitlab.com';
onMount(() => {
oauthIdEl && oauthIdEl.focus();
@ -141,7 +141,7 @@
<div class="mx-auto max-w-4xl px-6">
<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>
{#if $appSession.isAdmin}
<button
@ -228,20 +228,22 @@
bind:value={source.apiUrl}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
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."
/>
</div>
{#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
<input
name="customPort"
id="customPort"
disabled={!selfHosted}
readonly={!selfHosted}
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."
/>
</div>
{/if}
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<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 flex-wrap justify-center px-2 md:flex-row">
{#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
class="box-selection group relative hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
@ -128,10 +128,10 @@
</div>
{#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">
{#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
class="box-selection group hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}

View File

@ -92,7 +92,7 @@ label {
}
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 {
@ -104,7 +104,7 @@ a {
}
.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 {
@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);
}
}
.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",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.1.3",
"version": "3.2.0",
"license": "AGPL-3.0",
"repository": "github:coollabsio/coolify",
"scripts": {
"oc": "opencollective-setup",
"db:studio": "pnpm run --filter coolify-api db:studio",
"db:push": "pnpm run --filter coolify-api db:push",
"db:seed": "pnpm run --filter coolify-api db:seed",
@ -18,13 +20,14 @@
"build": "NODE_ENV=production run-p -n build:*",
"build:api": "NODE_ENV=production pnpm run --filter coolify-api 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: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": {
"cross-var": "1.1.0",
"npm-run-all": "4.1.5"
"npm-run-all": "4.1.5",
"opencollective-setup": "1.4.1"
},
"keywords": [
"docker",

929
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff