diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 90b882e41..bd98d3f2a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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", diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 326d434b3..3338ead6a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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... diff --git a/.gitpod.yml b/.gitpod.yml index 3af3611be..d46244e67 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -9,3 +9,5 @@ tasks: ports: - port: 3001 visibility: public + - port: 3000 + visibility: public diff --git a/Dockerfile b/Dockerfile index 1a2ced973..00c29b5f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ . diff --git a/README.md b/README.md index b08d7dfa6..40d7ac2dc 100644 --- a/README.md +++ b/README.md @@ -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 + + + +### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. + + + + + + + + + + + + +--- ## 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) + + ### 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. + + + + + + + + + + + + + + + + + +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 + + + + + + -### 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 diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..be0d3d48f --- /dev/null +++ b/TODO.md @@ -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 \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 9a37a9366..32a012685 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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", diff --git a/apps/api/prisma/migrations/20220718083646_moodle/migration.sql b/apps/api/prisma/migrations/20220718083646_moodle/migration.sql new file mode 100644 index 000000000..eaed14a4a --- /dev/null +++ b/apps/api/prisma/migrations/20220718083646_moodle/migration.sql @@ -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"); diff --git a/apps/api/prisma/migrations/20220718114551_remote_docker_engine/migration.sql b/apps/api/prisma/migrations/20220718114551_remote_docker_engine/migration.sql new file mode 100644 index 000000000..e80df22f2 --- /dev/null +++ b/apps/api/prisma/migrations/20220718114551_remote_docker_engine/migration.sql @@ -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; diff --git a/apps/api/prisma/migrations/20220721084020_ssh_key/migration.sql b/apps/api/prisma/migrations/20220721084020_ssh_key/migration.sql new file mode 100644 index 000000000..8323af409 --- /dev/null +++ b/apps/api/prisma/migrations/20220721084020_ssh_key/migration.sql @@ -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; diff --git a/apps/api/prisma/migrations/20220722203927_ipaddress/migration.sql b/apps/api/prisma/migrations/20220722203927_ipaddress/migration.sql new file mode 100644 index 000000000..21d45ada3 --- /dev/null +++ b/apps/api/prisma/migrations/20220722203927_ipaddress/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "ipv4" TEXT; +ALTER TABLE "Setting" ADD COLUMN "ipv6" TEXT; diff --git a/apps/api/prisma/migrations/20220725191205_architecture/migration.sql b/apps/api/prisma/migrations/20220725191205_architecture/migration.sql new file mode 100644 index 000000000..2e0ff3e01 --- /dev/null +++ b/apps/api/prisma/migrations/20220725191205_architecture/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "arch" TEXT; diff --git a/apps/api/prisma/migrations/20220726121333_fix_ssh_key/migration.sql b/apps/api/prisma/migrations/20220726121333_fix_ssh_key/migration.sql new file mode 100644 index 000000000..e6e47b197 --- /dev/null +++ b/apps/api/prisma/migrations/20220726121333_fix_ssh_key/migration.sql @@ -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; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 8d1e0bbfe..3b68829e3 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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]) } diff --git a/apps/api/prisma/seed.js b/apps/api/prisma/seed.js index 78a625e17..96b55b105 100644 --- a/apps/api/prisma/seed.js +++ b/apps/api/prisma/seed.js @@ -24,7 +24,8 @@ async function main() { data: { isRegistrationEnabled: true, proxyPassword: encrypt(generatePassword()), - proxyUser: cuid() + proxyUser: cuid(), + arch: process.arch } }); } else { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e302df635..60f83a50f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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) { } +} diff --git a/apps/api/src/jobs/checkProxies.ts b/apps/api/src/jobs/checkProxies.ts index 6168baf5a..761554061 100644 --- a/apps/api/src/jobs/checkProxies.ts +++ b/apps/api/src/jobs/checkProxies.ts @@ -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); } diff --git a/apps/api/src/jobs/cleanupStorage.ts b/apps/api/src/jobs/cleanupStorage.ts index dd8636175..97683ac2d 100644 --- a/apps/api/src/jobs/cleanupStorage.ts +++ b/apps/api/src/jobs/cleanupStorage.ts @@ -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); diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index b5dbe24d9..e31ccf149 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -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 }); diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 1feb33822..3c34c5f31 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -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 = []; 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 = []; 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 = []; 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 }); } \ No newline at end of file diff --git a/apps/api/src/lib/buildPacks/docker.ts b/apps/api/src/lib/buildPacks/docker.ts index 88d900c38..04041b190 100644 --- a/apps/api/src/lib/buildPacks/docker.ts +++ b/apps/api/src/lib/buildPacks/docker.ts @@ -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; } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 656075d8e..30743af3c 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -10,12 +10,14 @@ import crypto from 'crypto'; import { promises as dns } from 'dns'; import { PrismaClient } from '@prisma/client'; import cuid from 'cuid'; +import os from 'os'; +import sshConfig from 'ssh-config' -import { checkContainer, getEngine, removeContainer } from './docker'; +import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; import * as serviceFields from './serviceFields' -export const version = '3.1.3'; +export const version = '3.2.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -29,7 +31,7 @@ const customConfig: Config = { export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; -export const defaultTraefikImage = `traefik:v2.6`; +export const defaultTraefikImage = `traefik:v2.8`; export function getAPIUrl() { if (process.env.GITPOD_WORKSPACE_URL) { const { href } = new URL(process.env.GITPOD_WORKSPACE_URL) @@ -113,164 +115,164 @@ export const encrypt = (text: string) => { }; export const supportedServiceTypesAndVersions = [ - { - name: 'plausibleanalytics', - fancyName: 'Plausible Analytics', - baseImage: 'plausible/analytics', - images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], - versions: ['latest', 'stable'], - recommendedVersion: 'stable', - ports: { - main: 8000 - } - }, - { - name: 'nocodb', - fancyName: 'NocoDB', - baseImage: 'nocodb/nocodb', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'minio', - fancyName: 'MinIO', - baseImage: 'minio/minio', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 9001 - } - }, - { - name: 'vscodeserver', - fancyName: 'VSCode Server', - baseImage: 'codercom/code-server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'wordpress', - fancyName: 'Wordpress', - baseImage: 'wordpress', - images: ['bitnami/mysql:5.7'], - versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'vaultwarden', - fancyName: 'Vaultwarden', - baseImage: 'vaultwarden/server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'languagetool', - fancyName: 'LanguageTool', - baseImage: 'silviof/docker-languagetool', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8010 - } - }, - { - name: 'n8n', - fancyName: 'n8n', - baseImage: 'n8nio/n8n', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 5678 - } - }, - { - name: 'uptimekuma', - fancyName: 'Uptime Kuma', - baseImage: 'louislam/uptime-kuma', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 3001 - } - }, - { - name: 'ghost', - fancyName: 'Ghost', - baseImage: 'bitnami/ghost', - images: ['bitnami/mariadb'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 2368 - } - }, - { - name: 'meilisearch', - fancyName: 'Meilisearch', - baseImage: 'getmeili/meilisearch', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 7700 - } - }, - { - name: 'umami', - fancyName: 'Umami', - baseImage: 'ghcr.io/mikecao/umami', - images: ['postgres:12-alpine'], - versions: ['postgresql-latest'], - recommendedVersion: 'postgresql-latest', - ports: { - main: 3000 - } - }, - { - name: 'hasura', - fancyName: 'Hasura', - baseImage: 'hasura/graphql-engine', - images: ['postgres:12-alpine'], - versions: ['latest', 'v2.5.1'], - recommendedVersion: 'v2.5.1', - ports: { - main: 8080 - } - }, - { - name: 'fider', - fancyName: 'Fider', - baseImage: 'getfider/fider', - images: ['postgres:12-alpine'], - versions: ['stable'], - recommendedVersion: 'stable', - ports: { - main: 3000 - } - }, - // { - // name: 'moodle', - // fancyName: 'Moodle', - // baseImage: 'bitnami/moodle', - // images: [], - // versions: ['latest', 'v4.0.2'], - // recommendedVersion: 'latest', - // ports: { - // main: 8080 - // } - // } + { + name: 'plausibleanalytics', + fancyName: 'Plausible Analytics', + baseImage: 'plausible/analytics', + images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], + versions: ['latest', 'stable'], + recommendedVersion: 'stable', + ports: { + main: 8000 + } + }, + { + name: 'nocodb', + fancyName: 'NocoDB', + baseImage: 'nocodb/nocodb', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, + { + name: 'minio', + fancyName: 'MinIO', + baseImage: 'minio/minio', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 9001 + } + }, + { + name: 'vscodeserver', + fancyName: 'VSCode Server', + baseImage: 'codercom/code-server', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, + { + name: 'wordpress', + fancyName: 'Wordpress', + baseImage: 'wordpress', + images: ['bitnami/mysql:5.7'], + versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], + recommendedVersion: 'latest', + ports: { + main: 80 + } + }, + { + name: 'vaultwarden', + fancyName: 'Vaultwarden', + baseImage: 'vaultwarden/server', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 80 + } + }, + { + name: 'languagetool', + fancyName: 'LanguageTool', + baseImage: 'silviof/docker-languagetool', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8010 + } + }, + { + name: 'n8n', + fancyName: 'n8n', + baseImage: 'n8nio/n8n', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 5678 + } + }, + { + name: 'uptimekuma', + fancyName: 'Uptime Kuma', + baseImage: 'louislam/uptime-kuma', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 3001 + } + }, + { + name: 'ghost', + fancyName: 'Ghost', + baseImage: 'bitnami/ghost', + images: ['bitnami/mariadb'], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 2368 + } + }, + { + name: 'meilisearch', + fancyName: 'Meilisearch', + baseImage: 'getmeili/meilisearch', + images: [], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 7700 + } + }, + { + name: 'umami', + fancyName: 'Umami', + baseImage: 'ghcr.io/mikecao/umami', + images: ['postgres:12-alpine'], + versions: ['postgresql-latest'], + recommendedVersion: 'postgresql-latest', + ports: { + main: 3000 + } + }, + { + name: 'hasura', + fancyName: 'Hasura', + baseImage: 'hasura/graphql-engine', + images: ['postgres:12-alpine'], + versions: ['latest', 'v2.8.4', 'v2.5.1'], + recommendedVersion: 'v2.8.4', + ports: { + main: 8080 + } + }, + { + name: 'fider', + fancyName: 'Fider', + baseImage: 'getfider/fider', + images: ['postgres:12-alpine'], + versions: ['stable'], + recommendedVersion: 'stable', + ports: { + main: 3000 + } + }, + // { + // name: 'moodle', + // fancyName: 'Moodle', + // baseImage: 'bitnami/moodle', + // images: [], + // versions: ['latest', 'v4.0.2'], + // recommendedVersion: 'latest', + // ports: { + // main: 8080 + // } + // } ]; export async function checkDoubleBranch(branch: string, projectId: number): Promise { @@ -315,11 +317,13 @@ export function getDomain(domain: string): string { export async function isDomainConfigured({ id, fqdn, - checkOwn = false + checkOwn = false, + dockerId = undefined }: { id: string; fqdn: string; checkOwn?: boolean; + dockerId?: string; }): Promise { const domain = getDomain(fqdn); const nakedDomain = domain.replace('www.', ''); @@ -329,7 +333,10 @@ export async function isDomainConfigured({ { fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//www.${nakedDomain}` } } ], - id: { not: id } + id: { not: id }, + destinationDocker: { + id: dockerId + } }, select: { fqdn: true } }); @@ -341,7 +348,10 @@ export async function isDomainConfigured({ { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } ], - id: { not: checkOwn ? undefined : id } + id: { not: checkOwn ? undefined : id }, + destinationDocker: { + id: dockerId + } }, select: { fqdn: true } }); @@ -359,12 +369,9 @@ export async function isDomainConfigured({ return !!(foundApp || foundService || coolifyFqdn); } -export async function getContainerUsage(engine: string, container: string): Promise { - const host = getEngine(engine); +export async function getContainerUsage(dockerId: string, container: string): Promise { try { - const { stdout } = await asyncExecShell( - `DOCKER_HOST="${host}" docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` - ); + const { stdout } = await executeDockerCmd({ dockerId, command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` }) return JSON.parse(stdout); } catch (err) { return { @@ -453,7 +460,7 @@ export const supportedDatabaseTypesAndVersions = [ name: 'mariadb', fancyName: 'MariaDB', baseImage: 'bitnami/mariadb', - versions: ['10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] + versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] }, { name: 'postgresql', @@ -465,30 +472,100 @@ export const supportedDatabaseTypesAndVersions = [ name: 'redis', fancyName: 'Redis', baseImage: 'bitnami/redis', - versions: ['6.2', '6.0', '5.0'] + versions: ['7.0', '6.2', '6.0', '5.0'] }, { name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }, { name: 'edgedb', fancyName: 'EdgeDB', baseImage: 'edgedb/edgedb', - versions: ['2.0', '1.4'], - } + versions: ['2.0', '1.4'] + } ]; +export async function createRemoteEngineConfiguration(id: string) { + const homedir = os.homedir(); + const sshKeyFile = `/tmp/id_rsa-${id}` + const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }) + await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }) + // Needed for remote docker compose + const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep ssh-agent.pid | grep -v grep | wc -l`) + if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) { + await asyncExecShell(`eval $(ssh-agent -sa /tmp/ssh-agent.pid)`) + } + await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${sshKeyFile}`) -export async function startTraefikProxy(engine: string): Promise { - const host = getEngine(engine); - const found = await checkContainer(engine, 'coolify-proxy', true); - const { proxyPassword, proxyUser, id } = await listSettings(); + const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -fNL 11122:localhost:22' | grep -v grep | wc -l`) + console.log(numberOfSSHTunnelsRunning) + if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) { + try { + await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -fNL 11122:localhost:22 ${remoteIpAddress}`) + + } catch(error){ + console.log(error) + } + + } + + + const config = sshConfig.parse('') + const found = config.find({ Host: remoteIpAddress }) if (!found) { - const { stdout: Config } = await asyncExecShell( - `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` - ); + config.append({ + Host: remoteIpAddress, + Hostname: 'localhost', + Port: '11122', + User: remoteUser, + IdentityFile: sshKeyFile, + StrictHostKeyChecking: 'no' + }) + } + try { + await fs.stat(`${homedir}/.ssh/`) + } catch (error) { + await fs.mkdir(`${homedir}/.ssh/`) + } + + await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)) + + return + +} +export async function executeDockerCmd({ dockerId, command }: { dockerId: string, command: string }) { + let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + if (remoteEngine) { + await createRemoteEngineConfiguration(dockerId) + engine = `ssh://${remoteIpAddress}` + } else { + engine = 'unix:///var/run/docker.sock' + } + return await asyncExecShell( + `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` + ); + +} +export async function startTraefikProxy(id: string): Promise { + const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) + const found = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true }); + const { id: settingsId, ipv4, ipv6 } = await listSettings(); + + if (!found) { + const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }) const ip = JSON.parse(Config)[0].Gateway; - await asyncExecShell( - `DOCKER_HOST="${host}" docker run --restart always \ + let traefikUrl = mainTraefikEndpoint + if (remoteEngine) { + let ip = null + if (isDev) { + ip = getAPIUrl() + } else { + ip = `http://${ipv4 || ipv6}:3000` + } + traefikUrl = `${ip}/webhooks/traefik/remote/${id}` + } + await executeDockerCmd({ + dockerId: id, + command: `docker run --restart always \ --add-host 'host.docker.internal:host-gateway' \ - --add-host 'host.docker.internal:${ip}' \ + ${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \ -v coolify-traefik-letsencrypt:/etc/traefik/acme \ -v /var/run/docker.sock:/var/run/docker.sock \ --network coolify-infra \ @@ -502,96 +579,72 @@ export async function startTraefikProxy(engine: string): Promise { --entrypoints.websecure.forwardedHeaders.insecure=true \ --providers.docker=true \ --providers.docker.exposedbydefault=false \ - --providers.http.endpoint=${mainTraefikEndpoint} \ + --providers.http.endpoint=${traefikUrl} \ --providers.http.pollTimeout=5s \ --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ --log.level=error` - ); - await prisma.setting.update({ where: { id }, data: { proxyHash: null } }); - await prisma.destinationDocker.updateMany({ - where: { engine }, + }) + await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } }); + await prisma.destinationDocker.update({ + where: { id }, data: { isCoolifyProxyUsed: true } }); } - await configureNetworkTraefikProxy(engine); -} - -export async function configureNetworkTraefikProxy(engine: string): Promise { - const host = getEngine(engine); - const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); - const { stdout: networks } = await asyncExecShell( - `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` - ); - const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); - for (const destination of destinations) { - if (!configuredNetworks.includes(destination.network)) { - await asyncExecShell( - `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-proxy` - ); + // Configure networks for local docker engine + if (engine) { + const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); + for (const destination of destinations) { + await configureNetworkTraefikProxy(destination); + } + } + // Configure networks for remote docker engine + if (remoteEngine) { + const destinations = await prisma.destinationDocker.findMany({ where: { remoteIpAddress } }); + for (const destination of destinations) { + await configureNetworkTraefikProxy(destination); } } } +export async function configureNetworkTraefikProxy(destination: any): Promise { + const { id } = destination + const { stdout: networks } = await executeDockerCmd({ + dockerId: id, + command: + `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + }); + const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); + if (!configuredNetworks.includes(destination.network)) { + await executeDockerCmd({ dockerId: destination.id, command: `docker network connect ${destination.network} coolify-proxy` }) + } +} + export async function stopTraefikProxy( - engine: string + id: string ): Promise<{ stdout: string; stderr: string } | Error> { - const host = getEngine(engine); - const found = await checkContainer(engine, 'coolify-proxy'); - await prisma.destinationDocker.updateMany({ - where: { engine }, + const found = await checkContainer({ dockerId: id, container: 'coolify-proxy' }); + await prisma.destinationDocker.update({ + where: { id }, data: { isCoolifyProxyUsed: false } }); - const { id } = await prisma.setting.findFirst({}); - await prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + const { id: settingsId } = await prisma.setting.findFirst({}); + await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } }); try { if (found) { - await asyncExecShell( - `DOCKER_HOST="${host}" docker stop -t 0 coolify-proxy && docker rm coolify-proxy` - ); + await executeDockerCmd({ + dockerId: id, + command: + `docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + }); + } } catch (error) { return error; } } -export async function startCoolifyProxy(engine: string): Promise { - const host = getEngine(engine); - const found = await checkContainer(engine, 'coolify-haproxy', true); - const { proxyPassword, proxyUser, id } = await listSettings(); - if (!found) { - const { stdout: Config } = await asyncExecShell( - `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` - ); - const ip = JSON.parse(Config)[0].Gateway; - await asyncExecShell( - `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` - ); - await prisma.setting.update({ where: { id }, data: { proxyHash: null } }); - await prisma.destinationDocker.updateMany({ - where: { engine }, - data: { isCoolifyProxyUsed: true } - }); - } - await configureNetworkCoolifyProxy(engine); -} - -export async function configureNetworkCoolifyProxy(engine: string): Promise { - const host = getEngine(engine); - const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); - const { stdout: networks } = await asyncExecShell( - `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --format '{{json .Networks}}'` - ); - const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); - for (const destination of destinations) { - if (!configuredNetworks.includes(destination.network)) { - await asyncExecShell( - `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy` - ); - } - } -} export async function listSettings(): Promise { const settings = await prisma.setting.findFirst({}); if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword); @@ -599,29 +652,6 @@ export async function listSettings(): Promise { } - -// export async function stopCoolifyProxy( -// engine: string -// ): Promise<{ stdout: string; stderr: string } | Error> { -// const host = getEngine(engine); -// const found = await checkContainer(engine, 'coolify-haproxy'); -// await prisma.destinationDocker.updateMany({ -// where: { engine }, -// data: { isCoolifyProxyUsed: false } -// }); -// const { id } = await prisma.setting.findFirst({}); -// await prisma.setting.update({ where: { id }, data: { proxyHash: null } }); -// try { -// if (found) { -// await asyncExecShell( -// `DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy` -// ); -// } -// } catch (error) { -// return error; -// } -// } - export function generatePassword(length = 24, symbols = false): string { return generator.generate({ length, @@ -807,7 +837,7 @@ export function generateDatabaseConfiguration(database: any): }, image: `${baseImage}:${version}`, volume: `${id}-${type}-data:/edgedb/edgedb`, - ulimits: {}, + ulimits: {} }; } } @@ -929,38 +959,6 @@ export const createDirectories = async ({ }; }; -export async function startTcpProxy( - destinationDocker: any, - id: string, - publicPort: number, - privatePort: number -): Promise<{ stdout: string; stderr: string } | Error> { - const { network, engine } = destinationDocker; - const host = getEngine(engine); - - const containerName = `haproxy-for-${publicPort}`; - const found = await checkContainer(engine, containerName, true); - const foundDependentContainer = await checkContainer(engine, id, true); - try { - if (foundDependentContainer && !found) { - const { stdout: Config } = await asyncExecShell( - `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` - ); - const ip = JSON.parse(Config)[0].Gateway; - return await asyncExecShell( - `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}` - ); - } - if (!foundDependentContainer && found) { - return await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` - ); - } - } catch (error) { - return error; - } -} - export async function stopDatabaseContainer( database: any @@ -969,17 +967,15 @@ export async function stopDatabaseContainer( const { id, destinationDockerId, - destinationDocker: { engine } + destinationDocker: { engine, id: dockerId } } = database; if (destinationDockerId) { try { - const host = getEngine(engine); - 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 (stdout) { everStarted = true; - await removeContainer({ id, engine }); + await removeContainer({ id, dockerId }); } } catch (error) { // @@ -995,21 +991,18 @@ export async function stopTcpHttpProxy( publicPort: number, forceName: string = null ): Promise<{ stdout: string; stderr: string } | Error> { - const { engine } = destinationDocker; - const host = getEngine(engine); - const settings = await listSettings(); - let containerName = `${id}-${publicPort}`; - if (!settings.isTraefikUsed) { - containerName = `haproxy-for-${publicPort}`; - } - if (forceName) containerName = forceName; - const found = await checkContainer(engine, containerName); - + const { id: dockerId } = destinationDocker; + let container = `${id}-${publicPort}`; + if (forceName) container = forceName; + const found = await checkContainer({ dockerId, container }); try { if (found) { - return await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` - ); + return await executeDockerCmd({ + dockerId, + command: + `docker stop -t 0 ${container} && docker rm ${container}` + }); + } } catch (error) { return error; @@ -1027,66 +1020,99 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { dbUserPassword, defaultDatabase, destinationDockerId, - destinationDocker: { engine } + destinationDocker: { id: dockerId } } = database; if (destinationDockerId) { - const host = getEngine(engine); if (type === 'mysql') { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` + }) } else if (type === 'mariadb') { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` + }) + } else if (type === 'postgresql') { if (isRoot) { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"` + }) } else { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"` + }) } } else if (type === 'mongodb') { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"` + }) + } else if (type === 'redis') { - await asyncExecShell( - `DOCKER_HOST=${host} docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}` - ); + await executeDockerCmd({ + dockerId, + command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}` + }) + } } } +export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) { + const { default: getPort } = await import('get-port'); + const applicationUsed = await ( + await prisma.application.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const serviceUsed = await ( + await prisma.service.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const usedPorts = [...applicationUsed, ...serviceUsed]; + if (remoteIpAddress) { + const { default: checkPort } = await import('is-port-reachable'); + const found = await checkPort(exposePort, { host: remoteIpAddress }); + if (!found) { + return exposePort + } + return false + } + return await getPort({ port: Number(exposePort), exclude: usedPorts }); -export async function getFreePort() { +} +export async function getFreePublicPort(id, dockerId) { const { default: getPort, portNumbers } = await import('get-port'); const data = await prisma.setting.findFirst(); const { minPort, maxPort } = data; const dbUsed = await ( await prisma.database.findMany({ - where: { publicPort: { not: null } }, + where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, select: { publicPort: true } }) ).map((a) => a.publicPort); const wpFtpUsed = await ( await prisma.wordpress.findMany({ - where: { ftpPublicPort: { not: null } }, + where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, select: { ftpPublicPort: true } }) ).map((a) => a.ftpPublicPort); const wpUsed = await ( await prisma.wordpress.findMany({ - where: { mysqlPublicPort: { not: null } }, + where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, select: { mysqlPublicPort: true } }) ).map((a) => a.mysqlPublicPort); const minioUsed = await ( await prisma.minio.findMany({ - where: { publicPort: { not: null } }, + where: { publicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, select: { publicPort: true } }) ).map((a) => a.publicPort); @@ -1094,7 +1120,6 @@ export async function getFreePort() { return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); } - export async function startTraefikTCPProxy( destinationDocker: any, id: string, @@ -1102,34 +1127,48 @@ export async function startTraefikTCPProxy( privatePort: number, type?: string ): Promise<{ stdout: string; stderr: string } | Error> { - const { network, engine } = destinationDocker; - const host = getEngine(engine); - const containerName = `${id}-${publicPort}`; - const found = await checkContainer(engine, containerName, true); + const { network, id: dockerId, remoteEngine } = destinationDocker; + const container = `${id}-${publicPort}`; + const found = await checkContainer({ dockerId, container, remove: true }); + const { ipv4, ipv6 } = await listSettings(); + let dependentId = id; if (type === 'wordpressftp') dependentId = `${id}-ftp`; - const foundDependentContainer = await checkContainer(engine, dependentId, true); + const foundDependentContainer = await checkContainer({ dockerId, container: dependentId, remove: true }); try { if (foundDependentContainer && !found) { - const { stdout: Config } = await asyncExecShell( - `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` - ); + const { stdout: Config } = await executeDockerCmd({ + dockerId, + command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` + }) + const ip = JSON.parse(Config)[0].Gateway; + let traefikUrl = otherTraefikEndpoint + if (remoteEngine) { + let ip = null + if (isDev) { + ip = getAPIUrl() + } else { + ip = `http://${ipv4 || ipv6}:3000` + } + traefikUrl = `${ip}/webhooks/traefik/other.json` + } + console.log(traefikUrl) const tcpProxy = { - version: '3.5', + version: '3.8', services: { [`${id}-${publicPort}`]: { - container_name: containerName, - image: 'traefik:v2.6', + container_name: container, + image: defaultTraefikImage, command: [ `--entrypoints.tcp.address=:${publicPort}`, `--entryPoints.tcp.forwardedHeaders.insecure=true`, - `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`, + `--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`, '--providers.http.pollTimeout=2s', '--log.level=error' ], ports: [`${publicPort}:${publicPort}`], - extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal: ${ip}`], volumes: ['/var/run/docker.sock:/var/run/docker.sock'], networks: ['coolify-infra', network] } @@ -1146,15 +1185,17 @@ export async function startTraefikTCPProxy( } }; await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); - await asyncExecShell( - `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` - ); + await executeDockerCmd({ + dockerId, + command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d` + }) await fs.rm(`/tmp/docker-compose-${id}.yaml`); } if (!foundDependentContainer && found) { - return await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` - ); + await executeDockerCmd({ + dockerId, + command: `docker stop -t 0 ${container} && docker rm ${container}` + }) } } catch (error) { console.log(error); @@ -1379,7 +1420,7 @@ export async function configureServiceType({ } else if (type === 'moodle') { const defaultUsername = cuid(); const defaultPassword = encrypt(generatePassword()); - const defaultEmail = `${cuid()}@example.com`; + const defaultEmail = `${cuid()} @example.com`; const mariadbUser = cuid(); const mariadbPassword = encrypt(generatePassword()); const mariadbDatabase = 'moodle_db'; @@ -1490,9 +1531,9 @@ export const getServiceMainPort = (service: string) => { export function makeLabelForServices(type) { return [ 'coolify.managed=true', - `coolify.version=${version}`, - `coolify.type=service`, - `coolify.service.type=${type}` + `coolify.version = ${version} `, + `coolify.type = service`, + `coolify.service.type = ${type} ` ]; } export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { @@ -1518,8 +1559,7 @@ export async function stopBuild(buildId, applicationId) { let count = 0; await new Promise(async (resolve, reject) => { const { destinationDockerId, status } = await prisma.build.findFirst({ where: { id: buildId } }); - const { engine } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } }); - const host = getEngine(engine); + const { engine, id: dockerId } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } }); let interval = setInterval(async () => { try { if (status === 'failed') { @@ -1530,17 +1570,14 @@ export async function stopBuild(buildId, applicationId) { clearInterval(interval); return reject(new Error('Build canceled')); } - - const { stdout: buildContainers } = await asyncExecShell( - `DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` - ); + const { stdout: buildContainers } = await executeDockerCmd({ dockerId, command: `docker container ls--filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` }) if (buildContainers) { const containersArray = buildContainers.trim().split('\n'); for (const container of containersArray) { const containerObj = JSON.parse(container); const id = containerObj.ID; - if (!containerObj.Names.startsWith(`${applicationId}`)) { - await removeContainer({ id, engine }); + if (!containerObj.Names.startsWith(`${applicationId} `)) { + await removeContainer({ id, dockerId }); await cleanupDB(buildId); clearInterval(interval); return resolve(); @@ -1568,16 +1605,15 @@ export function convertTolOldVolumeNames(type) { // export async function getAvailableServices(): Promise { // const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`) // return data -// } -export async function cleanupDockerStorage(host, lowDiskSpace, force) { +// +export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { // Cleanup old coolify images try { - let { stdout: images } = await asyncExecShell( - `DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs ` - ); + let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs` }) + images = images.trim(); if (images) { - await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`); + await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs` }) } } catch (error) { //console.log(error); @@ -1588,17 +1624,17 @@ export async function cleanupDockerStorage(host, lowDiskSpace, force) { return } try { - await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`); + await executeDockerCmd({ dockerId, command: `docker container prune -f` }) } catch (error) { //console.log(error); } try { - await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`); + await executeDockerCmd({ dockerId, command: `docker image prune -f` }) } catch (error) { //console.log(error); } try { - await asyncExecShell(`DOCKER_HOST=${host} docker image prune -a -f`); + await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }) } catch (error) { //console.log(error); } diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index 730666590..db2dbc3e7 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -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 { - 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 { 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 { +export async function isContainerExited(dockerId: string, containerName: string): Promise { 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 { - 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); diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 5cb32db22..838f7f9b4 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -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) { } - 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) { + 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) { 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, 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, 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) { + 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) { 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) { 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) { }) } } catch ({ status, message }) { + console.log({ status, message }) return errorHandler({ status, message }) } } export async function getApplicationLogs(request: FastifyRequest) { 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 }) } diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index aa359f973..2f698ddeb 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -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 => { fastify.addHook('onRequest', async (request) => { @@ -17,9 +17,14 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id', async (request, reply) => await saveApplication(request, reply)); fastify.delete('/:id', async (request, reply) => await deleteApplication(request, reply)); + fastify.get('/:id/status', async (request) => await getApplicationStatus(request)); + fastify.post('/:id/stop', async (request, reply) => await stopApplication(request, reply)); + fastify.post('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); fastify.post('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply)); + + fastify.get('/:id/check', async (request) => await checkDomain(request)); fastify.post('/:id/check', async (request) => await checkDNS(request)); fastify.get('/:id/secrets', async (request) => await getSecrets(request)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 2d9b9913d..40dabc20a 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -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, + } +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/base/index.ts b/apps/api/src/routes/api/v1/base/index.ts index 9eaa8c40a..76735a032 100644 --- a/apps/api/src/routes/api/v1/base/index.ts +++ b/apps/api/src/routes/api/v1/base/index.ts @@ -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 => { 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, diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 8d05bb3a5..ca5fc67c0 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -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) { + 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) { try { const { id } = request.params; @@ -69,29 +95,11 @@ export async function getDatabase(request: FastifyRequest) { } 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) { 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) { 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) { 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) { } export async function getDatabaseLogs(request: FastifyRequest) { 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 => { fastify.post('/:id', async (request, reply) => await saveDatabase(request, reply)); fastify.delete('/:id', async (request) => await deleteDatabase(request)); + fastify.get('/:id/status', async (request) => await getDatabaseStatus(request)); + fastify.post('/:id/settings', async (request) => await saveDatabaseSettings(request)); fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request)); diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index 40d68a84b..81fcaf28e 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -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) { 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) { 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) { 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) { } export async function newDestination(request: FastifyRequest, 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) { 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) { - 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) { - 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) { - 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) { + 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 }) } diff --git a/apps/api/src/routes/api/v1/destinations/index.ts b/apps/api/src/routes/api/v1/destinations/index.ts index 43440cc1c..007242695 100644 --- a/apps/api/src/routes/api/v1/destinations/index.ts +++ b/apps/api/src/routes/api/v1/destinations/index.ts @@ -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 => { fastify.addHook('onRequest', async (request) => { return await request.jwtVerify() }) - fastify.get('/', async (request) => await listDestinations(request)); + fastify.get('/', async (request) => await listDestinations(request)); fastify.post('/check', async (request) => await checkDestination(request)); fastify.get('/:id', async (request) => await getDestination(request)); fastify.post('/:id', async (request, reply) => await newDestination(request, reply)); fastify.delete('/:id', async (request) => await deleteDestination(request)); + fastify.get('/:id/status', async (request) => await getDestinationStatus(request)); - fastify.post('/:id/settings', async (request, reply) => await saveDestinationSettings(request)); - fastify.post('/:id/start', async (request, reply) => await startProxy(request)); - fastify.post('/:id/stop', async (request, reply) => await stopProxy(request)); - fastify.post('/:id/restart', async (request, reply) => await restartProxy(request)); + fastify.post('/:id/settings', async (request) => await saveDestinationSettings(request)); + fastify.post('/:id/start', async (request,) => await startProxy(request)); + fastify.post('/:id/stop', async (request) => await stopProxy(request)); + fastify.post('/: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; diff --git a/apps/api/src/routes/api/v1/destinations/types.ts b/apps/api/src/routes/api/v1/destinations/types.ts index 25691b9d8..fe2742218 100644 --- a/apps/api/src/routes/api/v1/destinations/types.ts +++ b/apps/api/src/routes/api/v1/destinations/types.ts @@ -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 - } + } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 4b62f3845..69dd9b468 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -17,7 +17,8 @@ export async function hashPassword(password: string): Promise { 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, 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)) { diff --git a/apps/api/src/routes/api/v1/iam/handlers.ts b/apps/api/src/routes/api/v1/iam/handlers.ts index 7009ec794..72d648793 100644 --- a/apps/api/src/routes/api/v1/iam/handlers.ts +++ b/apps/api/src/routes/api/v1/iam/handlers.ts @@ -273,11 +273,15 @@ export async function inviteToTeam(request: FastifyRequest, 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 } } } diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 74658fc47..cb2b2abb7 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -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) { // 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) { + 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) { 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) { 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) { 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) { + 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) { 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) { 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) { }; 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) { 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) { } = 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) { }; 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) { 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) { } = 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) { 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) { 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) } = 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) } 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) { 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 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 }; 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) 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 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) { 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) { }; 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) { 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) 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) 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) 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) { } } = 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) { 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) { 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 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 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) 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) { } } = 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) { 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) { 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) { 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) { 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) { 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) { } } = 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) { 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) { 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) { } } = 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) { 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) { 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, 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 => { fastify.addHook('onRequest', async (request) => { @@ -41,6 +43,9 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id', async (request, reply) => await saveService(request, reply)); fastify.delete('/:id', async (request) => await deleteService(request)); + fastify.get('/:id/status', async (request) => await getServiceStatus(request)); + + fastify.get('/:id/check', async (request) => await checkServiceDomain(request)); fastify.post('/:id/check', async (request) => await checkService(request)); fastify.post('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply)); diff --git a/apps/api/src/routes/api/v1/services/types.ts b/apps/api/src/routes/api/v1/services/types.ts index 4ed631998..f09b4423f 100644 --- a/apps/api/src/routes/api/v1/services/types.ts +++ b/apps/api/src/routes/api/v1/services/types.ts @@ -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 } diff --git a/apps/api/src/routes/api/v1/settings/handlers.ts b/apps/api/src/routes/api/v1/settings/handlers.ts index 0c0c727d1..073dbd7e3 100644 --- a/apps/api/src/routes/api/v1/settings/handlers.ts +++ b/apps/api/src/routes/api/v1/settings/handlers.ts @@ -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) { 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) { } catch ({ status, message }) { return errorHandler({ status, message }) } +} + +export async function saveSSHKey(request: FastifyRequest, 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, 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 }) + } } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/settings/index.ts b/apps/api/src/routes/api/v1/settings/index.ts index f5181a14e..96da5948b 100644 --- a/apps/api/src/routes/api/v1/settings/index.ts +++ b/apps/api/src/routes/api/v1/settings/index.ts @@ -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 => { @@ -13,6 +13,9 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/check', async (request) => await checkDNS(request)); fastify.post('/check', async (request) => await checkDomain(request)); + + fastify.post('/sshKey', async (request, reply) => await saveSSHKey(request, reply)); + fastify.delete('/sshKey', async (request, reply) => await deleteSSHKey(request, reply)); }; export default root; diff --git a/apps/api/src/routes/api/v1/settings/types.ts b/apps/api/src/routes/api/v1/settings/types.ts index aa7398804..a33b614a4 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -28,4 +28,15 @@ export interface CheckDNS { Params: { domain: string, } +} +export interface SaveSSHKey { + Body: { + privateKey: string, + name: string + } +} +export interface DeleteSSHKey { + Body: { + id: string + } } \ No newline at end of file diff --git a/apps/api/src/routes/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index cb37029f4..968305a97 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -75,18 +75,18 @@ export async function gitHubEvents(request: FastifyRequest): 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): 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): 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): 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!' diff --git a/apps/api/src/routes/webhooks/github/types.ts b/apps/api/src/routes/webhooks/github/types.ts index c7502ec6d..1c6f6f9f4 100644 --- a/apps/api/src/routes/webhooks/github/types.ts +++ b/apps/api/src/routes/webhooks/github/types.ts @@ -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, + } } } } diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index dfe310ed7..0e7f8ec5d 100644 --- a/apps/api/src/routes/webhooks/gitlab/handlers.ts +++ b/apps/api/src/routes/webhooks/gitlab/handlers.ts @@ -34,7 +34,6 @@ export async function configureGitLabApp(request: FastifyRequest) { 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) { 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!' diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index 879ccd960..de805ee71 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -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 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 }) + } } \ No newline at end of file diff --git a/apps/api/src/routes/webhooks/traefik/index.ts b/apps/api/src/routes/webhooks/traefik/index.ts index 1d69be739..f9c7ff4b8 100644 --- a/apps/api/src/routes/webhooks/traefik/index.ts +++ b/apps/api/src/routes/webhooks/traefik/index.ts @@ -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 => { fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply)); fastify.get('/other.json', async (request, reply) => traefikOtherConfiguration(request)); + + fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request)); }; export default root; diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index 71f3db158..6367fd566 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -36,3 +36,4 @@ export interface SaveDatabaseSettings extends OnlyId { } + diff --git a/apps/ui/package.json b/apps/ui/package.json index babb6755c..10b2d5219 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -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", diff --git a/apps/ui/src/lib/locales/en.json b/apps/ui/src/lib/locales/en.json index cab9ecc80..df84a976b 100644 --- a/apps/ui/src/lib/locales/en.json +++ b/apps/ui/src/lib/locales/en.json @@ -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).

Databases will have their own proxy.", "add_new_destination": "Add New Destination", "predefined_destinations": "Predefined destinations" diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index 245e8d6eb..627885385 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -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 = writable() export const appSession: Writable = writable({ + ipv4: null, + ipv6: null, version: null, userId: null, teamId: null, @@ -31,7 +35,6 @@ export const appSession: Writable = writable({ gitlab: null } }); -export const isTraefikUsed: Writable = writable(false); export const disabledButton: Writable = writable(false); export const status: Writable = writable({ application: { @@ -41,14 +44,16 @@ export const status: Writable = 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 = 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 diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index a6932eae0..4df7f920b 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -65,11 +65,12 @@ -
@@ -62,7 +91,7 @@
Preview Deployments
- {application.name} + {application?.name}
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {/if} -
-
- Useful for creating staging environments." - : "These values overwrite application secrets in PR/MR deployments.
Useful for creating staging environments."} - /> -
- {#if applicationSecrets.length !== 0} - - - - - - - - - - - {#each applicationSecrets as secret} - {#key secret.id} - - s.name === secret.name)} - isPRMRSecret - name={secret.name} - value={secret.value} - isBuildSecret={secret.isBuildSecret} - on:refresh={refreshSecrets} - /> - - {/key} - {/each} - -
{$t('forms.name')}{$t('forms.value')}{$t('application.preview.need_during_buildtime')}{$t('forms.action')}
- {/if} -
- -
-
- {#if containers.length > 0} - {#each containers as container} - -
-
{getDomain(container.fqdn)}
-
-
-
- -
- {/each} - {:else} -
-
- {$t('application.preview.no_previews_available')} -
-
+{#if loading.init} + +{:else} +
+
+ Useful for creating staging environments." + : "These values overwrite application secrets in PR/MR deployments.
Useful for creating staging environments."} + /> +
+ {#if applicationSecrets.length !== 0} + + + + + + + + + + + {#each applicationSecrets as secret} + {#key secret.id} + + s.name === secret.name)} + isPRMRSecret + name={secret.name} + value={secret.value} + isBuildSecret={secret.isBuildSecret} + on:refresh={refreshSecrets} + /> + + {/key} + {/each} + +
{$t('forms.name')}{$t('forms.value')}{$t('application.preview.need_during_buildtime')}{$t('forms.action')}
{/if}
-
+ +
+
+ {#if containers.length > 0} + {#each containers as container} + +
+
{getDomain(container.fqdn)}
+
+
+
+ +
+
+ +
+ {/each} + {:else} +
+
+ {$t('application.preview.no_previews_available')} +
+
+ {/if} +
+
+{/if} diff --git a/apps/ui/src/routes/applications/index.svelte b/apps/ui/src/routes/applications/index.svelte index e24ecaf72..58644059e 100644 --- a/apps/ui/src/routes/applications/index.svelte +++ b/apps/ui/src/routes/applications/index.svelte @@ -93,7 +93,7 @@
{#each ownApplications as application} - +
{#if application.buildPack} {#if application.buildPack.toLowerCase() === 'rust'} @@ -140,15 +140,18 @@ {#if application.fqdn}
{getDomain(application.fqdn) || ''}
{/if} + {#if application.destinationDocker?.name} +
{application.destinationDocker.name}
+ {/if} {#if !application.gitSourceId || !application.repository || !application.branch}
Git Source Missing
- {:else if !application.destinationDockerId} + {:else if !application.destinationDockerId}
Destination Missing
- {:else if !application.fqdn} + {:else if !application.fqdn}
URL Missing
@@ -158,10 +161,10 @@ {/each}
{#if otherApplications.length > 0 && $appSession.teamId === '0'} -
Other Applications
+
Other Applications
{#each otherApplications as application} - +
{#if application.buildPack} {#if application.buildPack.toLowerCase() === 'rust'} diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte index c57520ee6..8127fd025 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -1,7 +1,6 @@ - +
+ +
@@ -44,40 +56,36 @@
-
- - -
- -
- - -
-
- -