diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index ea7c592ba..baf574184 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -2,7 +2,7 @@ name: production-release on: release: - types: [published] + types: [released] jobs: making-something-cool: diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 000000000..ba1de9b08 --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,39 @@ +name: release-candidate + +on: + release: + types: [prereleased] + +jobs: + making-something-cool: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: 'next' + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Get current package version + uses: martinbeentjes/npm-get-version-action@v1.2.3 + id: package-version + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: coollabsio/coolify:${{github.event.release.name}} + cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc + cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc,mode=max + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b64c7afba..958b6512f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ ### Add supported versions Supported versions are hardcoded into Coolify (for now). -You need to update `supportedServiceTypesAndVersions` function at [src/apps/api/src/lib/supportedVersions.ts](src/apps/api/src/lib/supportedVersions.ts). Example JSON: +You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON: ```js { @@ -208,22 +208,22 @@ ### Add required functions/properties 4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts) - -5. You need to add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts) +5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts) > See startUmamiService() function as example. +6. Add the newly added start process to `startService` in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts) -6. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts) +7. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts) SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. -7. You need to include it the logo at: +8. You need to include it the logo at: - [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`. - [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service -8. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte). +9. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte). If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore. diff --git a/CONTRIBUTION_NEW.md b/CONTRIBUTION_NEW.md new file mode 100644 index 000000000..a9ed208ed --- /dev/null +++ b/CONTRIBUTION_NEW.md @@ -0,0 +1,108 @@ +--- +head: + - - meta + - name: description + content: Coolify - Databases + - - meta + - name: keywords + content: databases coollabs coolify + - - meta + - name: twitter:card + content: summary_large_image + - - meta + - name: twitter:site + content: '@andrasbacsai' + - - meta + - name: twitter:title + content: Coolify + - - meta + - name: twitter:description + content: An open-source & self-hostable Heroku / Netlify alternative. + - - meta + - name: twitter:image + content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png + - - meta + - property: og:type + content: website + - - meta + - property: og:url + content: https://coolify.io + - - meta + - property: og:title + content: Coolify + - - meta + - property: og:description + content: An open-source & self-hostable Heroku / Netlify alternative. + - - meta + - property: og:site_name + content: Coolify + - - meta + - property: og:image + content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png +--- +# Contribution + +First, thanks for considering to contribute to my project. It really means a lot! :) + +You can ask for guidance anytime on our Discord server in the #contribution channel. + +## Setup your development environment +### Github codespaces + +If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already. + +### Gitpod + +If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already. + +### Local Machine +> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces. + +- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient! + +- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. +- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally. +- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally. + +Optional: +- To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally. + +### Inside a Docker container +`WIP` + +## Setup Coolify +- Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool. +- `pnpm install` to install dependencies. +- `pnpm db:push` to o create a local SQlite database. + + This will apply all migrations at `db/dev.db`. + +- `pnpm db:seed` seed the database. +- `pnpm dev` start coding. + +## Technical skills required + +- **Languages**: Node.js / Javascript / Typescript +- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/) +- **Database ORM**: [Prisma.io](https://www.prisma.io/) +- **Docker Engine API** + +## Add a new service +### Which service is eligable to add to Coolify? +The following statements needs to be true: + +- Self-hostable +- Open-source +- Maintained (I do not want to add software full of bugs) + +### Create Prisma / Database schema for the new service. +All data that needs to be persist for a service should be saved to the database in `cleartext` or `encrypted`. + +very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it. + +Update Prisma schema in [src/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma). + +- Add new model with the new service name. +- Make a relationship with `Service` model. +- In the `Service` model, the name of the new field should be with low-capital. +- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field. diff --git a/Dockerfile b/Dockerfile index 6be789f4a..938346439 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM node:18-alpine3.16 as build +FROM node:18-slim as build WORKDIR /app -RUN apk add --no-cache curl +RUN apt update && apt -y install curl RUN curl -sL https://unpkg.com/@pnpm/self-installer | node COPY . . @@ -9,21 +9,12 @@ RUN pnpm install RUN pnpm build # Production build -FROM node:18-alpine3.16 +FROM node:18-slim WORKDIR /app ENV NODE_ENV production ARG TARGETPLATFORM -ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \ - PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \ - PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \ - PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \ - PRISMA_CLI_QUERY_ENGINE_TYPE=binary \ - PRISMA_CLIENT_ENGINE_TYPE=binary - -COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/ - -RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl psmisc +RUN apt update && apt -y install git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 && apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN mkdir -p ~/.docker/cli-plugins/ diff --git a/apps/api/package.json b/apps/api/package.json index 838fad16c..f436482c8 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@breejs/ts-worker": "2.0.0", - "@fastify/autoload": "5.2.0", + "@fastify/autoload": "5.3.1", "@fastify/cookie": "8.1.0", "@fastify/cors": "8.1.0", "@fastify/env": "4.1.0", @@ -23,7 +23,7 @@ "@fastify/static": "6.5.0", "@iarna/toml": "2.2.5", "@ladjs/graceful": "3.0.2", - "@prisma/client": "3.15.2", + "@prisma/client": "4.3.1", "axios": "0.27.2", "bcryptjs": "2.4.3", "bree": "9.1.2", @@ -37,7 +37,7 @@ "fastify": "4.5.3", "fastify-plugin": "4.2.1", "generate-password": "1.7.0", - "got": "12.3.1", + "got": "12.4.1", "is-ip": "5.0.0", "is-port-reachable": "4.0.0", "js-yaml": "4.1.0", @@ -52,20 +52,20 @@ "unique-names-generator": "4.7.1" }, "devDependencies": { - "@types/node": "18.7.13", + "@types/node": "18.7.15", "@types/node-os-utils": "1.3.0", - "@typescript-eslint/eslint-plugin": "5.35.1", - "@typescript-eslint/parser": "5.35.1", - "esbuild": "0.15.5", + "@typescript-eslint/eslint-plugin": "5.36.2", + "@typescript-eslint/parser": "5.36.2", + "esbuild": "0.15.7", "eslint": "8.23.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "4.2.1", "nodemon": "2.0.19", "prettier": "2.7.1", - "prisma": "3.15.2", + "prisma": "4.3.1", "rimraf": "3.0.2", "tsconfig-paths": "4.1.0", - "typescript": "4.7.4" + "typescript": "4.8.2" }, "prisma": { "seed": "node prisma/seed.js" diff --git a/apps/api/prisma/migrations/20220831095714_service_weblate/migration.sql b/apps/api/prisma/migrations/20220831095714_service_weblate/migration.sql new file mode 100644 index 000000000..c985b4ae2 --- /dev/null +++ b/apps/api/prisma/migrations/20220831095714_service_weblate/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "Weblate" ( + "id" TEXT NOT NULL PRIMARY KEY, + "adminPassword" TEXT NOT NULL, + "postgresqlHost" TEXT NOT NULL, + "postgresqlPort" INTEGER NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Weblate_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Weblate_serviceId_key" ON "Weblate"("serviceId"); diff --git a/apps/api/prisma/migrations/20220902115640_service_taiga/migration.sql b/apps/api/prisma/migrations/20220902115640_service_taiga/migration.sql new file mode 100644 index 000000000..3035dd8ef --- /dev/null +++ b/apps/api/prisma/migrations/20220902115640_service_taiga/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "Taiga" ( + "id" TEXT NOT NULL PRIMARY KEY, + "secretKey" TEXT NOT NULL, + "erlangSecret" TEXT NOT NULL, + "djangoAdminPassword" TEXT NOT NULL, + "djangoAdminUser" TEXT NOT NULL, + "rabbitMQUser" TEXT NOT NULL, + "rabbitMQPassword" TEXT NOT NULL, + "postgresqlHost" TEXT NOT NULL, + "postgresqlPort" INTEGER NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Taiga_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Taiga_serviceId_key" ON "Taiga"("serviceId"); diff --git a/apps/api/prisma/migrations/20220905062318_database_branching/migration.sql b/apps/api/prisma/migrations/20220905062318_database_branching/migration.sql new file mode 100644 index 000000000..d828a4c66 --- /dev/null +++ b/apps/api/prisma/migrations/20220905062318_database_branching/migration.sql @@ -0,0 +1,22 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ApplicationSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "debug" BOOLEAN NOT NULL DEFAULT false, + "previews" BOOLEAN NOT NULL DEFAULT false, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "isBot" BOOLEAN NOT NULL DEFAULT false, + "isPublicRepository" BOOLEAN NOT NULL DEFAULT false, + "isDBBranching" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20220905113241_prisma_migration/migration.sql b/apps/api/prisma/migrations/20220905113241_prisma_migration/migration.sql new file mode 100644 index 000000000..324bcfff0 --- /dev/null +++ b/apps/api/prisma/migrations/20220905113241_prisma_migration/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to alter the column `time` on the `BuildLog` table. The data in that column could be lost. The data in that column will be cast from `Int` to `BigInt`. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_BuildLog" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT, + "buildId" TEXT NOT NULL, + "line" TEXT NOT NULL, + "time" BIGINT NOT NULL +); +INSERT INTO "new_BuildLog" ("applicationId", "buildId", "id", "line", "time") SELECT "applicationId", "buildId", "id", "line", "time" FROM "BuildLog"; +DROP TABLE "BuildLog"; +ALTER TABLE "new_BuildLog" RENAME TO "BuildLog"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20220905115321_application_connected_database/migration.sql b/apps/api/prisma/migrations/20220905115321_application_connected_database/migration.sql new file mode 100644 index 000000000..576c23bdf --- /dev/null +++ b/apps/api/prisma/migrations/20220905115321_application_connected_database/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "ApplicationConnectedDatabase" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "databaseId" TEXT, + "hostedDatabaseType" TEXT, + "hostedDatabaseHost" TEXT, + "hostedDatabasePort" INTEGER, + "hostedDatabaseName" TEXT, + "hostedDatabaseUser" TEXT, + "hostedDatabasePassword" TEXT, + "hostedDatabaseDBName" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationConnectedDatabase_databaseId_fkey" FOREIGN KEY ("databaseId") REFERENCES "Database" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ApplicationConnectedDatabase_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ApplicationConnectedDatabase_applicationId_key" ON "ApplicationConnectedDatabase"("applicationId"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index dfa7ae26b..64657e887 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl"] + binaryTargets = ["native"] } datasource db { @@ -117,6 +117,24 @@ model Application { settings ApplicationSettings? secrets Secret[] teams Team[] + connectedDatabase ApplicationConnectedDatabase? +} + +model ApplicationConnectedDatabase { + id String @id @default(cuid()) + applicationId String @unique + databaseId String? + hostedDatabaseType String? + hostedDatabaseHost String? + hostedDatabasePort Int? + hostedDatabaseName String? + hostedDatabaseUser String? + hostedDatabasePassword String? + hostedDatabaseDBName String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + database Database? @relation(fields: [databaseId], references: [id]) + application Application @relation(fields: [applicationId], references: [id]) } model ApplicationSettings { @@ -128,6 +146,7 @@ model ApplicationSettings { autodeploy Boolean @default(true) isBot Boolean @default(false) isPublicRepository Boolean @default(false) + isDBBranching Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt application Application @relation(fields: [applicationId], references: [id]) @@ -186,7 +205,7 @@ model BuildLog { applicationId String? buildId String line String - time Int + time BigInt } model Build { @@ -200,7 +219,7 @@ model Build { commit String? pullmergeRequestId String? forceRebuild Boolean @default(false) - sourceBranch String? + sourceBranch String? branch String? status String? @default("queued") createdAt DateTime @default(now()) @@ -291,22 +310,23 @@ model GitlabApp { } model Database { - id String @id @default(cuid()) - name String - publicPort Int? - defaultDatabase String? - type String? - version String? - dbUser String? - dbUserPassword String? - rootUser String? - rootUserPassword String? - destinationDockerId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - settings DatabaseSettings? - teams Team[] + id String @id @default(cuid()) + name String + publicPort Int? + defaultDatabase String? + type String? + version String? + dbUser String? + dbUserPassword String? + rootUser String? + rootUserPassword String? + destinationDockerId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + settings DatabaseSettings? + teams Team[] + applicationConnectedDatabase ApplicationConnectedDatabase[] } model DatabaseSettings { @@ -348,6 +368,8 @@ model Service { wordpress Wordpress? appwrite Appwrite? searxng Searxng? + weblate Weblate? + taiga Taiga? } model PlausibleAnalytics { @@ -559,3 +581,38 @@ model Searxng { updatedAt DateTime @updatedAt service Service @relation(fields: [serviceId], references: [id]) } + +model Weblate { + id String @id @default(cuid()) + adminPassword String + postgresqlHost String + postgresqlPort Int + postgresqlUser String + postgresqlPassword String + postgresqlDatabase String + postgresqlPublicPort Int? + serviceId String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) +} + +model Taiga { + id String @id @default(cuid()) + secretKey String + erlangSecret String + djangoAdminPassword String + djangoAdminUser String + rabbitMQUser String + rabbitMQPassword String + postgresqlHost String + postgresqlPort Int + postgresqlUser String + postgresqlPassword String + postgresqlDatabase String + postgresqlPublicPort Int? + serviceId String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 8c81b0350..4b3b3998f 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, listSettings, prisma, version } from './lib/common'; +import { asyncExecShell, createRemoteEngineConfiguration, isDev, listSettings, prisma, version } from './lib/common'; import { scheduler } from './lib/scheduler'; import { compareVersions } from 'compare-versions'; import Graceful from '@ladjs/graceful' @@ -136,8 +136,11 @@ fastify.listen({ port, host }, async (err: any, address: any) => { // scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines") // }, 60000) - await getArch(); - await getIPAddress(); + await Promise.all([ + getArch(), + getIPAddress(), + // configureRemoteDockers(), + ]) }); async function getIPAddress() { const { publicIpv4, publicIpv6 } = await import('public-ip') @@ -175,4 +178,15 @@ async function getArch() { } catch (error) { } } - +async function configureRemoteDockers() { + try { + const remoteDocker = await prisma.destinationDocker.findMany({ + where: { remoteVerified: true, remoteEngine: true } + }); + if (remoteDocker.length > 0) { + for (const docker of remoteDocker) { + await createRemoteEngineConfiguration(docker.id) + } + } + } catch (error) { } +} diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index dc3b0d41d..7ec322f63 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -177,9 +177,7 @@ import * as buildpacks from '../lib/buildPacks'; try { await prisma.build.update({ where: { id: buildId }, data: { commit } }); - } catch (err) { - console.log(err); - } + } catch (err) { } if (!pullmergeRequestId) { if (configHash !== currentHash) { diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index 19ab713aa..fbcb07616 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -1,11 +1,8 @@ import { parentPort } from 'node:worker_threads'; import axios from 'axios'; import { compareVersions } from 'compare-versions'; -import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common'; +import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common'; -async function disconnect() { - await prisma.$disconnect(); -} async function autoUpdater() { try { const currentVersion = version; @@ -24,9 +21,11 @@ async function autoUpdater() { const activeCount = 0 if (activeCount === 0) { if (!isDev) { - console.log(`Updating Coolify to ${latestVersion}.`); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env` + ); await asyncExecShell( `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` ); @@ -35,9 +34,7 @@ async function autoUpdater() { } } } - } catch (error) { - console.log(error); - } + } catch (error) { } } async function checkProxies() { try { @@ -45,18 +42,35 @@ async function checkProxies() { let portReachable; const { arch, ipv4, ipv6 } = await listSettings(); + // Coolify Proxy local const engine = '/var/run/docker.sock'; const localDocker = await prisma.destinationDocker.findFirst({ - where: { engine, network: 'coolify' } + where: { engine, network: 'coolify', isCoolifyProxyUsed: true } }); - if (localDocker && localDocker.isCoolifyProxyUsed) { + if (localDocker) { portReachable = await isReachable(80, { host: ipv4 || ipv6 }) if (!portReachable) { await startTraefikProxy(localDocker.id); } } - + // Coolify Proxy remote + const remoteDocker = await prisma.destinationDocker.findMany({ + where: { remoteEngine: true, remoteVerified: true } + }); + if (remoteDocker.length > 0) { + for (const docker of remoteDocker) { + if (docker.isCoolifyProxyUsed) { + portReachable = await isReachable(80, { host: docker.remoteIpAddress }) + if (!portReachable) { + await startTraefikProxy(docker.id); + } + } + try { + await createRemoteEngineConfiguration(docker.id) + } catch (error) { } + } + } // TCP Proxies const databasesWithPublicPort = await prisma.database.findMany({ where: { publicPort: { not: null } }, @@ -113,9 +127,7 @@ async function cleanupPrismaEngines() { if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) { await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`) } - } catch (error) { - console.log(error); - } + } catch (error) { } } } async function cleanupStorage() { @@ -166,9 +178,7 @@ async function cleanupStorage() { lowDiskSpace = true; } } - } catch (error) { - console.log(error); - } + } catch (error) { } await cleanupDockerStorage(destination.id, lowDiskSpace, false) } } diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 97bfb6fb9..b6e57f52c 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -512,7 +512,6 @@ export async function copyBaseConfigurationFiles( ); } } catch (error) { - console.log(error); throw new Error(error); } } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index f38dfef82..90b35cd12 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -21,7 +21,7 @@ import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.8.9'; +export const version = '3.9.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -139,10 +139,10 @@ export const prisma = new PrismaClient({ }); // prisma.$on('query', (e) => { - // console.log({e}) - // console.log('Query: ' + e.query) - // console.log('Params: ' + e.params) - // console.log('Duration: ' + e.duration + 'ms') +// console.log({e}) +// console.log('Query: ' + e.query) +// console.log('Params: ' + e.params) +// console.log('Duration: ' + e.duration + 'ms') // }) export const base64Encode = (text: string): string => { return Buffer.from(text).toString('base64'); @@ -439,7 +439,6 @@ export async function getFreeSSHLocalPort(id: string): Promise return Number(alreadyConfigured.sshLocalPort) } const range = generateRangeArray(minPort, maxPort) - console.log({ ports }) const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port)) for (const port of availablePorts) { const found = await isReachable(port, { host: 'localhost' }) @@ -458,20 +457,21 @@ export async function createRemoteEngineConfiguration(id: string) { 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`) + const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`) if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) { - await asyncExecShell(`eval $(ssh-agent -sa /tmp/ssh-agent.pid)`) + try { + await fs.stat(`/tmp/coolify-ssh-agent.pid`) + await fs.rm(`/tmp/coolify-ssh-agent.pid`) + } catch (error) { } + await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`) } - await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${sshKeyFile}`) + await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`) const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`) if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) { try { - await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`) - - } catch (error) { - console.log(error) - } + await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`) + } catch (error) { } } const config = sshConfig.parse('') @@ -1089,7 +1089,6 @@ export async function checkExposedPort({ id, configuredPort, exposePort, dockerI if (exposePort < 1024 || exposePort > 65535) { throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } } - if (configuredPort) { if (configuredPort !== exposePort) { const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); @@ -1249,7 +1248,6 @@ export async function startTraefikTCPProxy( }) } } catch (error) { - console.log(error); return error; } } @@ -1309,6 +1307,9 @@ export function saveUpdateableFields(type: string, data: any) { temp = Boolean(temp) } } + if (k.isNumber && temp === '') { + temp = null + } update[k.name] = temp }); } @@ -1351,9 +1352,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 }) { @@ -1434,15 +1435,13 @@ export function convertTolOldVolumeNames(type) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { // Cleanup old coolify images try { - let { stdout: images } = await executeDockerCmd({ dockerId, command: `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 -r` }) images = images.trim(); if (images) { - await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs` }) + await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` }) } - } catch (error) { - //console.log(error); - } + } catch (error) { } if (lowDiskSpace || force) { if (isDev) { if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`); @@ -1450,37 +1449,40 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { } try { await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }) - } catch (error) { - //console.log(error); - } + } catch (error) { } try { await executeDockerCmd({ dockerId, command: `docker image prune -f` }) - } catch (error) { - //console.log(error); - } + } catch (error) { } try { await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }) - } catch (error) { - //console.log(error); - } + } catch (error) { } // Cleanup build caches try { await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }) - } catch (error) { - //console.log(error); - } + } catch (error) { } } } export function persistentVolumes(id, persistentStorage, config) { + let volumeSet = new Set(); + if (Object.keys(config).length > 0) { + for (const [key, value] of Object.entries(config)) { + if (value.volumes) { + for (const volume of value.volumes) { + volumeSet.add(volume); + } + } + + } + } + const volumesArray = Array.from(volumeSet); const persistentVolume = persistentStorage?.map((storage) => { return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; }) || []; let volumes = [...persistentVolume] - if (config.volume) volumes = [config.volume, ...volumes] - + if (volumesArray) volumes = [...volumesArray, ...volumes] const composeVolumes = volumes.length > 0 && volumes.map((volume) => { return { [`${volume.split(':')[0]}`]: { @@ -1489,16 +1491,11 @@ export function persistentVolumes(id, persistentStorage, config) { }; }) || [] - const volumeMounts = config.volume && Object.assign( + const volumeMounts = Object.assign( {}, - { - [config.volume.split(':')[0]]: { - name: config.volume.split(':')[0] - } - }, ...composeVolumes ) || {} - return { volumes, volumeMounts } + return { volumeMounts } } export function defaultComposeConfiguration(network: string): any { return { @@ -1515,26 +1512,26 @@ export function defaultComposeConfiguration(network: string): any { } } export function decryptApplication(application: any) { - if (application) { - if (application?.gitSource?.githubApp?.clientSecret) { - application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; - } - if (application?.gitSource?.githubApp?.webhookSecret) { - application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null; - } - if (application?.gitSource?.githubApp?.privateKey) { - application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null; - } - if (application?.gitSource?.gitlabApp?.appSecret) { - application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null; - } - if (application?.secrets.length > 0) { - application.secrets = application.secrets.map((s: any) => { - s.value = decrypt(s.value) || null - return s; - }); - } + if (application) { + if (application?.gitSource?.githubApp?.clientSecret) { + application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; + } + if (application?.gitSource?.githubApp?.webhookSecret) { + application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null; + } + if (application?.gitSource?.githubApp?.privateKey) { + application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null; + } + if (application?.gitSource?.gitlabApp?.appSecret) { + application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null; + } + if (application?.secrets.length > 0) { + application.secrets = application.secrets.map((s: any) => { + s.value = decrypt(s.value) || null + return s; + }); + } - return application; - } + return application; + } } diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index c1ae977e5..f0b13d5f1 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -76,7 +76,6 @@ export async function removeContainer({ await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) } } catch (error) { - console.log(error); throw error; } } diff --git a/apps/api/src/lib/services/common.ts b/apps/api/src/lib/services/common.ts index 44caeabe9..f6174ca69 100644 --- a/apps/api/src/lib/services/common.ts +++ b/apps/api/src/lib/services/common.ts @@ -1,23 +1,7 @@ -import { exec } from 'node:child_process' -import util from 'util'; -import fs from 'fs/promises'; -import yaml from 'js-yaml'; -import forge from 'node-forge'; -import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; -import type { Config } from 'unique-names-generator'; -import generator from 'generate-password'; -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 { encrypt, generatePassword, prisma } from '../common'; - -export const version = '3.8.2'; -export const isDev = process.env.NODE_ENV === 'development'; - export const includeServices: any = { destinationDocker: true, persistentStorage: true, @@ -34,7 +18,9 @@ export const includeServices: any = { moodle: true, appwrite: true, glitchTip: true, - searxng: true + searxng: true, + weblate: true, + taiga: true }; export async function configureServiceType({ id, @@ -312,6 +298,58 @@ export async function configureServiceType({ } } }); + } else if (type === 'weblate') { + const adminPassword = encrypt(generatePassword({})) + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'weblate'; + await prisma.service.update({ + where: { id }, + data: { + type, + weblate: { + create: { + adminPassword, + postgresqlHost: `${id}-postgresql`, + postgresqlPort: 5432, + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + } + } + } + }); + } else if (type === 'taiga') { + const secretKey = encrypt(generatePassword({})) + const erlangSecret = encrypt(generatePassword({})) + const rabbitMQUser = cuid(); + const djangoAdminUser = cuid(); + const djangoAdminPassword = encrypt(generatePassword({})) + const rabbitMQPassword = encrypt(generatePassword({})) + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'taiga'; + await prisma.service.update({ + where: { id }, + data: { + type, + taiga: { + create: { + secretKey, + erlangSecret, + djangoAdminUser, + djangoAdminPassword, + rabbitMQUser, + rabbitMQPassword, + postgresqlHost: `${id}-postgresql`, + postgresqlPort: 5432, + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + } + } + } + }); } else { await prisma.service.update({ where: { id }, @@ -338,7 +376,8 @@ export async function removeService({ id }: { id: string }): Promise { await prisma.moodle.deleteMany({ where: { serviceId: id } }); await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.searxng.deleteMany({ where: { serviceId: id } }); - + await prisma.weblate.deleteMany({ where: { serviceId: id } }); + await prisma.taiga.deleteMany({ where: { serviceId: id } }); await prisma.service.delete({ where: { id } }); } \ No newline at end of file diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index cbefd7a94..d66747e67 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -3,7 +3,7 @@ import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; import { ServiceStartStop } from '../../routes/api/v1/services/types'; -import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, makeLabelForServices, persistentVolumes, prisma } from '../common'; +import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { defaultServiceConfigurations } from '../services'; export async function startService(request: FastifyRequest) { @@ -63,6 +63,12 @@ export async function startService(request: FastifyRequest) { if (type === 'searxng') { return await startSearXNGService(request) } + if (type === 'weblate') { + return await startWeblateService(request) + } + if (type === 'taiga') { + return await startTaigaService(request) + } throw `Service type ${type} not supported.` } catch (error) { throw { status: 500, message: error?.message || error } @@ -119,7 +125,7 @@ async function startPlausibleAnalyticsService(request: FastifyRequest) { const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-nc:/usr/app/data`, - environmentVariables: {} + nocodb: { + image: `${image}:${version}`, + volumes: [`${id}-nc:/usr/app/data`], + environmentVariables: {} + } + }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.nocodb.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, + image: config.nocodb.image, + volumes: config.nocodb.volumes, + environment: config.nocodb.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), ...defaultComposeConfiguration(network), @@ -329,29 +329,32 @@ async function startMinioService(request: FastifyRequest) { const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-minio-data:/data`, - environmentVariables: { - MINIO_ROOT_USER: rootUser, - MINIO_ROOT_PASSWORD: rootUserPassword, - MINIO_BROWSER_REDIRECT_URL: fqdn + minio: { + image: `${image}:${version}`, + volumes: [`${id}-minio-data:/data`], + environmentVariables: { + MINIO_ROOT_USER: rootUser, + MINIO_ROOT_PASSWORD: rootUserPassword, + MINIO_BROWSER_REDIRECT_URL: fqdn + } } + }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.minio.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, + image: config.minio.image, command: `server /data --console-address ":${consolePort}"`, - environment: config.environmentVariables, - volumes, + environment: config.minio.environmentVariables, + volumes: config.minio.volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('minio'), ...defaultComposeConfiguration(network), @@ -397,27 +400,30 @@ async function startVscodeService(request: FastifyRequest) { const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-vscodeserver-data:/home/coder`, - environmentVariables: { - PASSWORD: password + vscodeserver: { + image: `${image}:${version}`, + volumes: [`${id}-vscodeserver-data:/home/coder`], + environmentVariables: { + PASSWORD: password + } } + }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.vscodeserver.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - environment: config.environmentVariables, - volumes, + image: config.vscodeserver.image, + environment: config.vscodeserver.environmentVariables, + volumes: config.vscodeserver.volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vscodeServer'), ...defaultComposeConfiguration(network), @@ -432,7 +438,6 @@ async function startVscodeService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) const changePermissionOn = persistentStorage.map((p) => p.path); @@ -484,7 +489,7 @@ async function startWordpressService(request: FastifyRequest) const config = { wordpress: { image: `${image}:${version}`, - volume: `${id}-wordpress-data:/var/www/html`, + volumes: [`${id}-wordpress-data:/var/www/html`], environmentVariables: { WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, WORDPRESS_DB_USER: mysqlUser, @@ -495,7 +500,7 @@ async function startWordpressService(request: FastifyRequest) }, mysql: { image: `bitnami/mysql:5.7`, - volume: `${id}-mysql-data:/bitnami/mysql/data`, + volumes: [`${id}-mysql-data:/bitnami/mysql/data`], environmentVariables: { MYSQL_ROOT_PASSWORD: mysqlRootUserPassword, MYSQL_ROOT_USER: mysqlRootUser, @@ -507,7 +512,7 @@ async function startWordpressService(request: FastifyRequest) }; if (isARM(arch)) { config.mysql.image = 'mysql:5.7' - config.mysql.volume = `${id}-mysql-data:/var/lib/mysql` + config.mysql.volumes = [`${id}-mysql-data:/var/lib/mysql`] } if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { @@ -515,7 +520,7 @@ async function startWordpressService(request: FastifyRequest) }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', @@ -524,7 +529,7 @@ async function startWordpressService(request: FastifyRequest) container_name: id, image: config.wordpress.image, environment: config.wordpress.environmentVariables, - volumes, + volumes: config.wordpress.volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('wordpress'), ...defaultComposeConfiguration(network), @@ -542,20 +547,14 @@ async function startWordpressService(request: FastifyRequest) composeFile.services[`${id}-mysql`] = { container_name: `${id}-mysql`, image: config.mysql.image, - volumes: [config.mysql.volume], + volumes: config.mysql.volumes, environment: config.mysql.environmentVariables, ...defaultComposeConfiguration(network), }; - - composeFile.volumes[config.mysql.volume.split(':')[0]] = { - name: config.mysql.volume.split(':')[0] - }; } const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -577,24 +576,27 @@ async function startVaultwardenService(request: FastifyRequest const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-vaultwarden-data:/data/`, - environmentVariables: {} + vaultwarden: { + image: `${image}:${version}`, + volumes: [`${id}-vaultwarden-data:/data/`], + environmentVariables: {} + } + }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.vaultwarden.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - environment: config.environmentVariables, - volumes, + image: config.vaultwarden.image, + environment: config.vaultwarden.environmentVariables, + volumes: config.vaultwarden.volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), ...defaultComposeConfiguration(network), @@ -609,9 +611,7 @@ async function startVaultwardenService(request: FastifyRequest }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -632,26 +632,28 @@ async function startLanguageToolService(request: FastifyRequest 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.languagetool.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - environment: config.environmentVariables, + image: config.languagetool.image, + environment: config.languagetool.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes, + volumes: config.languagetool, labels: makeLabelForServices('languagetool'), ...defaultComposeConfiguration(network), } @@ -665,9 +667,7 @@ async function startLanguageToolService(request: FastifyRequest) { const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-n8n:/root/.n8n`, - environmentVariables: { - WEBHOOK_URL: `${service.fqdn}` + n8n: { + image: `${image}:${version}`, + volumes: [`${id}-n8n:/root/.n8n`], + environmentVariables: { + WEBHOOK_URL: `${service.fqdn}` + } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.n8n.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, + image: config.n8n.image, + volumes: config.n8n, + environment: config.n8n.environmentVariables, labels: makeLabelForServices('n8n'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...defaultComposeConfiguration(network), @@ -722,9 +724,7 @@ async function startN8nService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -745,24 +745,26 @@ async function startUptimekumaService(request: FastifyRequest) const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-uptimekuma:/app/data`, - environmentVariables: {} + uptimekuma: { + image: `${image}:${version}`, + volumes: [`${id}-uptimekuma:/app/data`], + environmentVariables: {} + } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.uptimekuma.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, + image: config.uptimekuma.image, + volumes: config.uptimekuma.volumes, + environment: config.uptimekuma.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), ...defaultComposeConfiguration(network), @@ -777,9 +779,7 @@ async function startUptimekumaService(request: FastifyRequest) }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -820,7 +820,7 @@ async function startGhostService(request: FastifyRequest) { const config = { ghost: { image: `${image}:${version}`, - volume: `${id}-ghost:/bitnami/ghost`, + volumes: [`${id}-ghost:/bitnami/ghost`], environmentVariables: { url: fqdn, GHOST_HOST: domain, @@ -836,7 +836,7 @@ async function startGhostService(request: FastifyRequest) { }, mariadb: { image: `bitnami/mariadb:latest`, - volume: `${id}-mariadb:/bitnami/mariadb`, + volumes: [`${id}-mariadb:/bitnami/mariadb`], environmentVariables: { MARIADB_USER: mariadbUser, MARIADB_PASSWORD: mariadbPassword, @@ -852,14 +852,14 @@ async function startGhostService(request: FastifyRequest) { }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.ghost.image, - volumes, + volumes: config.ghost.volumes, environment: config.ghost.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), @@ -869,7 +869,7 @@ async function startGhostService(request: FastifyRequest) { [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, - volumes: [config.mariadb.volume], + volumes: config.mariadb.volumes, environment: config.mariadb.environmentVariables, ...defaultComposeConfiguration(network), } @@ -879,18 +879,11 @@ async function startGhostService(request: FastifyRequest) { external: true } }, - volumes: { - ...volumeMounts, - [config.mariadb.volume.split(':')[0]]: { - name: config.mariadb.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -914,28 +907,30 @@ async function startMeilisearchService(request: FastifyRequest const image = getServiceImage(type); const config = { - image: `${image}:${version}`, - volume: `${id}-datams:/data.ms`, - environmentVariables: { - MEILI_MASTER_KEY: masterKey + meilisearch: { + image: `${image}:${version}`, + volumes: [`${id}-datams:/data.ms`], + environmentVariables: { + MEILI_MASTER_KEY: masterKey + } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; + config.meilisearch.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, - image: config.image, - environment: config.environmentVariables, + image: config.meilisearch.image, + environment: config.meilisearch.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes, + volumes: config.meilisearch.volumes, labels: makeLabelForServices('meilisearch'), ...defaultComposeConfiguration(network), } @@ -994,7 +989,7 @@ async function startUmamiService(request: FastifyRequest) { }, postgresql: { image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, @@ -1091,7 +1086,7 @@ async function startUmamiService(request: FastifyRequest) { FROM ${config.postgresql.image} COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) const composeFile: ComposeFile = { version: '3.8', services: { @@ -1099,7 +1094,6 @@ async function startUmamiService(request: FastifyRequest) { container_name: id, image: config.umami.image, environment: config.umami.environmentVariables, - volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('umami'), depends_on: [`${id}-postgresql`], @@ -1109,7 +1103,7 @@ async function startUmamiService(request: FastifyRequest) { build: workdir, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], + volumes: config.postgresql.volumes, ...defaultComposeConfiguration(network), } }, @@ -1118,12 +1112,7 @@ async function startUmamiService(request: FastifyRequest) { external: true } }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); @@ -1164,7 +1153,7 @@ async function startHasuraService(request: FastifyRequest) { }, postgresql: { image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, @@ -1178,7 +1167,7 @@ async function startHasuraService(request: FastifyRequest) { }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) const composeFile: ComposeFile = { version: '3.8', services: { @@ -1186,7 +1175,6 @@ async function startHasuraService(request: FastifyRequest) { container_name: id, image: config.hasura.image, environment: config.hasura.environmentVariables, - volumes, labels: makeLabelForServices('hasura'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], @@ -1196,7 +1184,7 @@ async function startHasuraService(request: FastifyRequest) { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], + volumes: config.postgresql.volumes, ...defaultComposeConfiguration(network), } }, @@ -1205,18 +1193,11 @@ async function startHasuraService(request: FastifyRequest) { external: true } }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -1278,7 +1259,7 @@ async function startFiderService(request: FastifyRequest) { }, postgresql: { image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, @@ -1291,7 +1272,7 @@ async function startFiderService(request: FastifyRequest) { config.fider.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) const composeFile: ComposeFile = { version: '3.8', services: { @@ -1299,7 +1280,6 @@ async function startFiderService(request: FastifyRequest) { container_name: id, image: config.fider.image, environment: config.fider.environmentVariables, - volumes, labels: makeLabelForServices('fider'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], @@ -1309,7 +1289,7 @@ async function startFiderService(request: FastifyRequest) { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], + volumes: config.postgresql.volumes, ...defaultComposeConfiguration(network), } }, @@ -1318,18 +1298,11 @@ async function startFiderService(request: FastifyRequest) { external: true } }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -1364,18 +1337,18 @@ async function startAppWriteService(request: FastifyRequest) { container_name: id, labels: makeLabelForServices('appwrite'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - "volumes": [ + volumes: [ `${id}-uploads:/storage/uploads:rw`, `${id}-cache:/storage/cache:rw`, `${id}-config:/storage/config:rw`, `${id}-certificates:/storage/certificates:rw`, `${id}-functions:/storage/functions:rw` ], - "depends_on": [ + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", "_APP_LOCALE=en", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, @@ -1403,11 +1376,11 @@ async function startAppWriteService(request: FastifyRequest) { container_name: `${id}-realtime`, entrypoint: "realtime", labels: makeLabelForServices('appwrite'), - "depends_on": [ + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1422,16 +1395,15 @@ async function startAppWriteService(request: FastifyRequest) { ...defaultComposeConfiguration(network), }, [`${id}-worker-audits`]: { - image: `${image}:${version}`, container_name: `${id}-worker-audits`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-audits", - "depends_on": [ + entrypoint: "worker-audits", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1449,12 +1421,12 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-webhooks`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-webhooks", - "depends_on": [ + entrypoint: "worker-webhooks", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1467,12 +1439,12 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-deletes`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-deletes", - "depends_on": [ + entrypoint: "worker-deletes", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "volumes": [ + volumes: [ `${id}-uploads:/storage/uploads:rw`, `${id}-cache:/storage/cache:rw`, `${id}-config:/storage/config:rw`, @@ -1500,12 +1472,12 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-databases`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-databases", - "depends_on": [ + entrypoint: "worker-databases", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1523,12 +1495,12 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-builds`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-builds", - "depends_on": [ + entrypoint: "worker-builds", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_EXECUTOR_SECRET=${executorSecret}`, @@ -1548,16 +1520,16 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-certificates`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-certificates", - "depends_on": [ + entrypoint: "worker-certificates", + depends_on: [ `${id}-mariadb`, `${id}-redis`, ], - "volumes": [ + volumes: [ `${id}-config:/storage/config:rw`, `${id}-certificates:/storage/certificates:rw`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_DOMAIN=${fqdn}`, @@ -1577,13 +1549,13 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-functions`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-functions", - "depends_on": [ + entrypoint: "worker-functions", + depends_on: [ `${id}-mariadb`, `${id}-redis`, `${id}-executor` ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1603,20 +1575,20 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-executor`, labels: makeLabelForServices('appwrite'), - "entrypoint": "executor", - "stop_signal": "SIGINT", - "volumes": [ + entrypoint: "executor", + stop_signal: "SIGINT", + volumes: [ `${id}-functions:/storage/functions:rw`, `${id}-builds:/storage/builds:rw`, "/var/run/docker.sock:/var/run/docker.sock", "/tmp:/tmp:rw" ], - "depends_on": [ + depends_on: [ `${id}-mariadb`, `${id}-redis`, `${id}` ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_EXECUTOR_SECRET=${executorSecret}`, ...secrets @@ -1627,11 +1599,11 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-mails`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-mails", - "depends_on": [ + entrypoint: "worker-mails", + depends_on: [ `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_REDIS_HOST=${id}-redis`, @@ -1644,11 +1616,11 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-worker-messaging`, labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-messaging", - "depends_on": [ + entrypoint: "worker-messaging", + depends_on: [ `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_REDIS_HOST=${id}-redis`, "_APP_REDIS_PORT=6379", @@ -1660,11 +1632,11 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-maintenance`, labels: makeLabelForServices('appwrite'), - "entrypoint": "maintenance", - "depends_on": [ + entrypoint: "maintenance", + depends_on: [ `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_DOMAIN=${fqdn}`, @@ -1684,11 +1656,11 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-schedule`, labels: makeLabelForServices('appwrite'), - "entrypoint": "schedule", - "depends_on": [ + entrypoint: "schedule", + depends_on: [ `${id}-redis`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_REDIS_HOST=${id}-redis`, "_APP_REDIS_PORT=6379", @@ -1697,27 +1669,27 @@ async function startAppWriteService(request: FastifyRequest) { ...defaultComposeConfiguration(network), }, [`${id}-mariadb`]: { - "image": "mariadb:10.7", + image: "mariadb:10.7", container_name: `${id}-mariadb`, labels: makeLabelForServices('appwrite'), - "volumes": [ + volumes: [ `${id}-mariadb:/var/lib/mysql:rw` ], - "environment": [ + environment: [ `MYSQL_ROOT_USER=${mariadbRootUser}`, `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, `MYSQL_USER=${mariadbUser}`, `MYSQL_PASSWORD=${mariadbPassword}`, `MYSQL_DATABASE=${mariadbDatabase}` ], - "command": "mysqld --innodb-flush-method=fsync", + command: "mysqld --innodb-flush-method=fsync", ...defaultComposeConfiguration(network), }, [`${id}-redis`]: { - "image": "redis:6.2-alpine", + image: "redis:6.2-alpine", container_name: `${id}-redis`, - "command": `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, - "volumes": [ + command: `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, + volumes: [ `${id}-redis:/data:rw` ], ...defaultComposeConfiguration(network), @@ -1730,12 +1702,12 @@ async function startAppWriteService(request: FastifyRequest) { image: `${image}:${version}`, container_name: `${id}-usage`, labels: makeLabelForServices('appwrite'), - "entrypoint": "usage", - "depends_on": [ + entrypoint: "usage", + depends_on: [ `${id}-mariadb`, `${id}-influxdb`, ], - "environment": [ + environment: [ "_APP_ENV=production", `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_DB_HOST=${mariadbHost}`, @@ -1752,17 +1724,17 @@ async function startAppWriteService(request: FastifyRequest) { ...defaultComposeConfiguration(network), } dockerCompose[`${id}-influxdb`] = { - "image": "appwrite/influxdb:1.5.0", + image: "appwrite/influxdb:1.5.0", container_name: `${id}-influxdb`, - "volumes": [ + volumes: [ `${id}-influxdb:/var/lib/influxdb:rw` ], ...defaultComposeConfiguration(network), } dockerCompose[`${id}-telegraf`] = { - "image": "appwrite/telegraf:1.4.0", + image: "appwrite/telegraf:1.4.0", container_name: `${id}-telegraf`, - "environment": [ + environment: [ `_APP_INFLUXDB_HOST=${id}-influxdb`, "_APP_INFLUXDB_PORT=8086", ], @@ -1811,9 +1783,7 @@ async function startAppWriteService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -1881,7 +1851,7 @@ async function startMoodleService(request: FastifyRequest) { const config = { moodle: { image: `${image}:${version}`, - volume: `${id}-data:/bitnami/moodle`, + volumes: [`${id}-data:/bitnami/moodle`], environmentVariables: { MOODLE_USERNAME: defaultUsername, MOODLE_PASSWORD: defaultPassword, @@ -1895,7 +1865,7 @@ async function startMoodleService(request: FastifyRequest) { }, mariadb: { image: 'bitnami/mariadb:latest', - volume: `${id}-mariadb-data:/bitnami/mariadb`, + volumes: [`${id}-mariadb-data:/bitnami/mariadb`], environmentVariables: { MARIADB_USER: mariadbUser, MARIADB_PASSWORD: mariadbPassword, @@ -1910,7 +1880,7 @@ async function startMoodleService(request: FastifyRequest) { config.moodle.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) const composeFile: ComposeFile = { version: '3.8', services: { @@ -1918,36 +1888,18 @@ async function startMoodleService(request: FastifyRequest) { container_name: id, image: config.moodle.image, environment: config.moodle.environmentVariables, - networks: [network], - volumes, - restart: 'always', + volumes: config.moodle.volumes, labels: makeLabelForServices('moodle'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [`${id}-mariadb`] + depends_on: [`${id}-mariadb`], + ...defaultComposeConfiguration(network), }, [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, environment: config.mariadb.environmentVariables, - networks: [network], - volumes: [], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, + volumes: config.mariadb.volumes, + ...defaultComposeConfiguration(network), depends_on: [] } @@ -1957,19 +1909,12 @@ async function startMoodleService(request: FastifyRequest) { external: true } }, - volumes: { - ...volumeMounts, - [config.mariadb.volume.split(':')[0]]: { - name: config.mariadb.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -2044,7 +1989,7 @@ async function startGlitchTipService(request: FastifyRequest) }, postgresql: { image: 'postgres:14-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, @@ -2053,7 +1998,7 @@ async function startGlitchTipService(request: FastifyRequest) }, redis: { image: 'redis:7-alpine', - volume: `${id}-redis-data:/data`, + volumes: [`${id}-redis-data:/data`], } }; if (serviceSecret.length > 0) { @@ -2061,7 +2006,7 @@ async function startGlitchTipService(request: FastifyRequest) config.glitchTip.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) const composeFile: ComposeFile = { version: '3.8', services: { @@ -2069,7 +2014,6 @@ async function startGlitchTipService(request: FastifyRequest) container_name: id, image: config.glitchTip.image, environment: config.glitchTip.environmentVariables, - volumes, labels: makeLabelForServices('glitchTip'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`, `${id}-redis`], @@ -2096,13 +2040,13 @@ async function startGlitchTipService(request: FastifyRequest) image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], + volumes: config.postgresql.volumes, ...defaultComposeConfiguration(network), }, [`${id}-redis`]: { image: config.redis.image, container_name: `${id}-redis`, - volumes: [config.redis.volume], + volumes: config.redis.volumes, ...defaultComposeConfiguration(network), } }, @@ -2111,22 +2055,12 @@ async function startGlitchTipService(request: FastifyRequest) external: true } }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - }, - [config.redis.volume.split(':')[0]]: { - name: config.redis.volume.split(':')[0] - } - } + volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - 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 }) @@ -2149,7 +2083,7 @@ async function startSearXNGService(request: FastifyRequest) { const config = { searxng: { image: `${image}:${version}`, - volume: `${id}-searxng:/etc/searxng`, + volumes: [`${id}-searxng:/etc/searxng`], environmentVariables: { SEARXNG_BASE_URL: `${fqdn}` }, @@ -2180,14 +2114,14 @@ async function startSearXNGService(request: FastifyRequest) { config.searxng.environmentVariables[secret.name] = secret.value; }); } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { build: workdir, container_name: id, - volumes, + volumes: config.searxng.volumes, environment: config.searxng.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('searxng'), @@ -2217,6 +2151,434 @@ async function startSearXNGService(request: FastifyRequest) { await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/settings.yml`, settingsYml); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + + +async function startWeblateService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + weblate: { adminPassword, postgresqlHost, postgresqlPort, postgresqlUser, postgresqlPassword, postgresqlDatabase } + } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('weblate'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + weblate: { + image: `${image}:${version}`, + volumes: [`${id}-data:/app/data`], + environmentVariables: { + WEBLATE_SITE_DOMAIN: getDomain(fqdn), + WEBLATE_ADMIN_PASSWORD: adminPassword, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_USER: postgresqlUser, + POSTGRES_DATABASE: postgresqlDatabase, + POSTGRES_HOST: postgresqlHost, + POSTGRES_PORT: postgresqlPort, + REDIS_HOST: `${id}-redis`, + } + }, + postgresql: { + image: `postgres:14-alpine`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], + environmentVariables: { + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_USER: postgresqlUser, + POSTGRES_DB: postgresqlDatabase, + POSTGRES_HOST: postgresqlHost, + POSTGRES_PORT: postgresqlPort, + } + }, + redis: { + image: `redis:6-alpine`, + volumes: [`${id}-redis-data:/data`], + } + + }; + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.weblate.environmentVariables[secret.name] = secret.value; + }); + } + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.weblate.image, + environment: config.weblate.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: config.weblate.volumes, + labels: makeLabelForServices('weblate'), + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + container_name: `${id}-postgresql`, + image: config.postgresql.image, + environment: config.postgresql.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: config.postgresql.volumes, + labels: makeLabelForServices('weblate'), + ...defaultComposeConfiguration(network), + }, + [`${id}-redis`]: { + container_name: `${id}-redis`, + image: config.redis.image, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: config.redis.volumes, + labels: makeLabelForServices('weblate'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startTaigaService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + taiga: { secretKey, djangoAdminUser, djangoAdminPassword, erlangSecret, rabbitMQUser, rabbitMQPassword, postgresqlHost, postgresqlPort, postgresqlUser, postgresqlPassword, postgresqlDatabase } + } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('taiga'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const isHttps = fqdn.startsWith('https://'); + const superUserEntrypoint = `#!/bin/sh + set -e + python manage.py makemigrations + python manage.py migrate + + if [ "$DJANGO_SUPERUSER_USERNAME" ] + then + python manage.py createsuperuser \ + --noinput \ + --username $DJANGO_SUPERUSER_USERNAME \ + --email $DJANGO_SUPERUSER_EMAIL + fi + exec "$@"`; + const entrypoint = `#!/bin/sh + set -e + + /taiga-back/docker/entrypoint_superuser.sh || echo "Superuser creation failed, but continue" + /taiga-back/docker/entrypoint.sh + + exec "$@"`; + + const DockerfileBack = ` + FROM taigaio/taiga-back:latest + COPY ./entrypoint_superuser.sh /taiga-back/docker/entrypoint_superuser.sh + COPY ./entrypoint_coolify.sh /taiga-back/docker/entrypoint_coolify.sh + RUN ["chmod", "+x", "/taiga-back/docker/entrypoint_superuser.sh"] + RUN ["chmod", "+x", "/taiga-back/docker/entrypoint_coolify.sh"] + RUN ["chmod", "+x", "/taiga-back/docker/entrypoint.sh"]`; + + const DockerfileGateway = ` + FROM nginx:1.19-alpine + COPY ./nginx.conf /etc/nginx/conf.d/default.conf`; + + const nginxConf = `server { + listen 80 default_server; + + client_max_body_size 100M; + charset utf-8; + + # Frontend + location / { + proxy_pass http://${id}-taiga-front/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # API + location /api/ { + proxy_pass http://${id}-taiga-back:8000/api/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # Admin + location /admin/ { + proxy_pass http://${id}-taiga-back:8000/admin/; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + } + + # Static + location /static/ { + alias /taiga/static/; + } + + # Media + location /_protected/ { + internal; + alias /taiga/media/; + add_header Content-disposition "attachment"; + } + + # Unprotected section + location /media/exports/ { + alias /taiga/media/exports/; + add_header Content-disposition "attachment"; + } + + location /media/ { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://${id}-taiga-protected:8003/; + proxy_redirect off; + } + + # Events + location /events { + proxy_pass http://${id}-taiga-events:8888/events; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + }` + await fs.writeFile(`${workdir}/entrypoint_superuser.sh`, superUserEntrypoint); + await fs.writeFile(`${workdir}/entrypoint_coolify.sh`, entrypoint); + await fs.writeFile(`${workdir}/DockerfileBack`, DockerfileBack); + await fs.writeFile(`${workdir}/DockerfileGateway`, DockerfileGateway); + await fs.writeFile(`${workdir}/nginx.conf`, nginxConf); + + const config = { + ['taiga-gateway']: { + volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], + }, + ['taiga-front']: { + image: `${image}:${version}`, + environmentVariables: { + TAIGA_URL: fqdn, + TAIGA_WEBSOCKETS_URL: isHttps ? `wss://${getDomain(fqdn)}` : `ws://${getDomain(fqdn)}`, + TAIGA_SUBPATH: "", + PUBLIC_REGISTER_ENABLED: isDev ? "true" : "false", + } + }, + ['taiga-back']: { + volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], + environmentVariables: { + POSTGRES_DB: postgresqlDatabase, + POSTGRES_HOST: postgresqlHost, + POSTGRES_PORT: postgresqlPort, + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + TAIGA_SECRET_KEY: secretKey, + TAIGA_SITES_SCHEME: isHttps ? 'https' : 'http', + TAIGA_SITES_DOMAIN: getDomain(fqdn), + TAIGA_SUBPATH: "", + EVENTS_PUSH_BACKEND_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, + CELERY_BROKER_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, + RABBITMQ_USER: rabbitMQUser, + RABBITMQ_PASS: rabbitMQPassword, + ENABLE_TELEMETRY: "False", + DJANGO_SUPERUSER_EMAIL: `admin@${getDomain(fqdn)}`, + DJANGO_SUPERUSER_PASSWORD: djangoAdminPassword, + DJANGO_SUPERUSER_USERNAME: djangoAdminUser, + PUBLIC_REGISTER_ENABLED: isDev ? "True" : "False", + SESSION_COOKIE_SECURE: isDev ? "False" : "True", + CSRF_COOKIE_SECURE: isDev ? "False" : "True", + + } + }, + ['taiga-async']: { + image: `taigaio/taiga-back:latest`, + volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], + environmentVariables: { + POSTGRES_DB: postgresqlDatabase, + POSTGRES_HOST: postgresqlHost, + POSTGRES_PORT: postgresqlPort, + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + TAIGA_SECRET_KEY: secretKey, + TAIGA_SITES_SCHEME: isHttps ? 'https' : 'http', + TAIGA_SITES_DOMAIN: getDomain(fqdn), + TAIGA_SUBPATH: "", + RABBITMQ_USER: rabbitMQUser, + RABBITMQ_PASS: rabbitMQPassword, + ENABLE_TELEMETRY: "False", + } + }, + ['taiga-rabbitmq']: { + image: `rabbitmq:3.8-management-alpine`, + volumes: [`${id}-events:/var/lib/rabbitmq`], + environmentVariables: { + RABBITMQ_ERLANG_COOKIE: erlangSecret, + RABBITMQ_DEFAULT_USER: rabbitMQUser, + RABBITMQ_DEFAULT_PASS: rabbitMQPassword, + RABBITMQ_DEFAULT_VHOST: 'taiga' + } + }, + ['taiga-protected']: { + image: `taigaio/taiga-protected:latest`, + environmentVariables: { + MAX_AGE: 360, + SECRET_KEY: secretKey, + TAIGA_URL: fqdn + } + }, + ['taiga-events']: { + image: `taigaio/taiga-events:latest`, + environmentVariables: { + RABBITMQ_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, + RABBITMQ_USER: rabbitMQUser, + RABBITMQ_PASS: rabbitMQPassword, + TAIGA_SECRET_KEY: secretKey, + } + }, + + postgresql: { + image: `postgres:12.3`, + volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], + environmentVariables: { + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_USER: postgresqlUser, + POSTGRES_DB: postgresqlDatabase + } + } + }; + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config['taiga-back'].environmentVariables[secret.name] = secret.value; + }); + } + const { volumeMounts } = persistentVolumes(id, persistentStorage, config) + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + build: { + context: '.', + dockerfile: 'DockerfileGateway', + }, + container_name: id, + volumes: config['taiga-gateway'].volumes, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-taiga-front`]: { + container_name: `${id}-taiga-front`, + image: config['taiga-front'].image, + environment: config['taiga-front'].environmentVariables, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-taiga-back`]: { + build: { + context: '.', + dockerfile: 'DockerfileBack', + }, + entrypoint: '/taiga-back/docker/entrypoint_coolify.sh', + container_name: `${id}-taiga-back`, + environment: config['taiga-back'].environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: config['taiga-back'].volumes, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + + [`${id}-async`]: { + container_name: `${id}-taiga-async`, + image: config['taiga-async'].image, + entrypoint: ["/taiga-back/docker/async_entrypoint.sh"], + environment: config['taiga-async'].environmentVariables, + volumes: config['taiga-async'].volumes, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-taiga-rabbitmq`]: { + container_name: `${id}-taiga-rabbitmq`, + image: config['taiga-rabbitmq'].image, + volumes: config['taiga-rabbitmq'].volumes, + environment: config['taiga-rabbitmq'].environmentVariables, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-taiga-protected`]: { + container_name: `${id}-taiga-protected`, + image: config['taiga-protected'].image, + environment: config['taiga-protected'].environmentVariables, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-taiga-events`]: { + container_name: `${id}-taiga-events`, + image: config['taiga-events'].image, + environment: config['taiga-events'].environmentVariables, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + container_name: `${id}-postgresql`, + image: config.postgresql.image, + environment: config.postgresql.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes: config.postgresql.volumes, + labels: makeLabelForServices('taiga'), + ...defaultComposeConfiguration(network), + }, + + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await startServiceContainers(destinationDocker.id, composeFileDestination) return {} @@ -2224,3 +2586,4 @@ async function startSearXNGService(request: FastifyRequest) { return errorHandler({ status, message }) } } + diff --git a/apps/api/src/lib/services/serviceFields.ts b/apps/api/src/lib/services/serviceFields.ts index a9d5b379a..22549e9a5 100644 --- a/apps/api/src/lib/services/serviceFields.ts +++ b/apps/api/src/lib/services/serviceFields.ts @@ -599,6 +599,54 @@ export const glitchTip = [{ isBoolean: false, isEncrypted: true }, +{ + name: 'emailSmtpHost', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'emailSmtpPassword', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'emailSmtpUseSsl', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: true, + isEncrypted: false +}, +{ + name: 'emailSmtpUseSsl', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: true, + isEncrypted: false +}, +{ + name: 'emailSmtpPort', + isEditable: true, + isLowerCase: false, + isNumber: true, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'emailSmtpUser', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, { name: 'defaultEmail', isEditable: false, @@ -624,7 +672,7 @@ export const glitchTip = [{ isEncrypted: true }, { - name: 'defaultFromEmail', + name: 'defaultEmailFrom', isEditable: true, isLowerCase: false, isNumber: false, @@ -687,4 +735,133 @@ export const searxng = [{ isNumber: false, isBoolean: false, isEncrypted: true +}] + +export const weblate = [{ + name: 'adminPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'postgresqlHost', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlPort', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'postgresqlDatabase', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}] +export const taiga = [{ + name: 'secretKey', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'djangoAdminUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'djangoAdminPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'rabbitMQUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'rabbitMQPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'postgresqlHost', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlPort', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'postgresqlPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'postgresqlDatabase', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false }] \ No newline at end of file diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts index b6f039695..145d7395b 100644 --- a/apps/api/src/lib/services/supportedVersions.ts +++ b/apps/api/src/lib/services/supportedVersions.ts @@ -190,4 +190,26 @@ export const supportedServiceTypesAndVersions = [ main: 8080 } }, + { + name: 'weblate', + fancyName: 'Weblate', + baseImage: 'weblate/weblate', + images: ['postgres:14-alpine', 'redis:6-alpine'], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, + // { + // name: 'taiga', + // fancyName: 'Taiga', + // baseImage: 'taigaio/taiga-front', + // images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'], + // versions: ['latest'], + // recommendedVersion: 'latest', + // ports: { + // main: 80 + // } + // }, ]; \ No newline at end of file diff --git a/apps/api/src/plugins/jwt.ts b/apps/api/src/plugins/jwt.ts index 54dd1b72d..029aecd94 100644 --- a/apps/api/src/plugins/jwt.ts +++ b/apps/api/src/plugins/jwt.ts @@ -21,7 +21,6 @@ export default fp(async (fastify, opts) => { try { await request.jwtVerify() } catch (err) { - console.log(err) reply.send(err) } }) diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index c83504f94..e37590381 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -3,16 +3,23 @@ import crypto from 'node:crypto' import jsonwebtoken from 'jsonwebtoken'; import axios from 'axios'; import { FastifyReply } from 'fastify'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; + import { day } from '../../../../lib/dayjs'; -import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; +import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, 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, CheckDomain, StopPreviewApplication } from './types'; import { OnlyId } from '../../../../types'; +function filterObject(obj, callback) { + return Object.fromEntries(Object.entries(obj). + filter(([key, val]) => callback(val, key))); +} + export async function listApplications(request: FastifyRequest) { try { const { teamId } = request.user @@ -149,7 +156,8 @@ export async function getApplicationFromDB(id: string, teamId: string) { settings: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, secrets: true, - persistentStorage: true + persistentStorage: true, + connectedDatabase: true } }); if (!application) { @@ -176,32 +184,39 @@ export async function getApplicationFromDB(id: string, teamId: string) { } export async function getApplicationFromDBWebhook(projectId: number, branch: string) { try { - let application = await prisma.application.findFirst({ + let applications = await prisma.application.findMany({ where: { projectId, branch, settings: { autodeploy: true } }, include: { destinationDocker: true, settings: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, secrets: true, - persistentStorage: true + persistentStorage: true, + connectedDatabase: true } }); - if (!application) { + if (applications.length === 0) { throw { status: 500, message: 'Application not configured.' } } - application = decryptApplication(application); - const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( - application.buildPack - ); + applications = applications.map((application: any) => { + application = decryptApplication(application); + const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( + application.buildPack + ); - // Set default build images - if (!application.baseImage) { - application.baseImage = baseImage; - } - if (!application.baseBuildImage) { - application.baseBuildImage = baseBuildImage; - } - return { ...application, baseBuildImages, baseImages }; + // Set default build images + if (!application.baseImage) { + application.baseImage = baseImage; + } + if (!application.baseBuildImage) { + application.baseBuildImage = baseBuildImage; + } + application.baseBuildImages = baseBuildImages; + application.baseImages = baseImages; + return application + }) + + return applications; } catch ({ status, message }) { return errorHandler({ status, message }) @@ -229,15 +244,16 @@ export async function saveApplication(request: FastifyRequest, denoOptions, baseImage, baseBuildImage, - deploymentType + deploymentType, + baseDatabaseBranch } = request.body if (port) port = Number(port); if (exposePort) { exposePort = Number(exposePort); } - const { destinationDocker: { id: dockerId, remoteIpAddress } } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) - if (exposePort) await checkExposedPort({ id, exposePort, dockerId, remoteIpAddress }) + const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) if (denoOptions) denoOptions = denoOptions.trim(); const defaultConfiguration = await setDefaultConfiguration({ buildPack, @@ -250,22 +266,43 @@ export async function saveApplication(request: FastifyRequest, dockerFileLocation, denoMainFile }); - await prisma.application.update({ - where: { id }, - data: { - name, - fqdn, - exposePort, - pythonWSGI, - pythonModule, - pythonVariable, - denoOptions, - baseImage, - baseBuildImage, - deploymentType, - ...defaultConfiguration - } - }); + if (baseDatabaseBranch) { + await prisma.application.update({ + where: { id }, + data: { + name, + fqdn, + exposePort, + pythonWSGI, + pythonModule, + pythonVariable, + denoOptions, + baseImage, + baseBuildImage, + deploymentType, + ...defaultConfiguration, + connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } + } + }); + } else { + await prisma.application.update({ + where: { id }, + data: { + name, + fqdn, + exposePort, + pythonWSGI, + pythonModule, + pythonVariable, + denoOptions, + baseImage, + baseBuildImage, + deploymentType, + ...defaultConfiguration + } + }); + } + return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }) @@ -276,15 +313,15 @@ export async function saveApplication(request: FastifyRequest, export async function saveApplicationSettings(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body - const isDouble = await checkDoubleBranch(branch, projectId); - if (isDouble && autodeploy) { - await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) - throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' } - } + const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body + // const isDouble = await checkDoubleBranch(branch, projectId); + // if (isDouble && autodeploy) { + // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) + // throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' } + // } await prisma.application.update({ where: { id }, - data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } }, + data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } }, include: { destinationDocker: true } }); return reply.code(201).send(); @@ -312,6 +349,113 @@ export async function stopPreviewApplication(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params + const { teamId } = request.user + let application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; + + const envs = [ + `PORT=${port}` + ]; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + if (secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + const { workdir } = await createDirectories({ repository, buildId }); + const labels = [] + let image = null + const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }) + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`) + } + }) + } + let imageFound = false; + try { + await executeDockerCmd({ + dockerId, + command: `docker image inspect ${image}` + }) + imageFound = true; + } catch (error) { + // + } + if (!imageFound) { + throw { status: 500, message: 'Image not found, cannot restart application.' } + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [applicationId]: { + image, + container_name: applicationId, + volumes, + env_file: envFound ? [`${workdir}/.env`] : [], + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) + await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) + return reply.code(201).send(); + } + throw { status: 500, message: 'Application cannot be restarted.' } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function stopApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params @@ -332,12 +476,14 @@ export async function stopApplication(request: FastifyRequest, reply: Fa export async function deleteApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params + const { force } = request.body + const { teamId } = request.user const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - if (application?.destinationDockerId && application.destinationDocker?.network) { + if (!force && application?.destinationDockerId && application.destinationDocker?.network) { const { stdout: containers } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` @@ -356,6 +502,7 @@ export async function deleteApplication(request: FastifyRequest) { } export async function checkDNS(request: FastifyRequest) { try { + const { id } = request.params let { exposePort, fqdn, forceSave, dualCerts } = request.body - - if (fqdn) fqdn = fqdn.toLowerCase(); + if (!fqdn) { + return {} + } else { + 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 } }) @@ -558,12 +709,12 @@ export async function saveRepository(request, reply) { data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } } }); } - if (!isPublicRepository) { - const isDouble = await checkDoubleBranch(branch, projectId); - if (isDouble) { - await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) - } - } + // if (!isPublicRepository) { + // const isDouble = await checkDoubleBranch(branch, projectId); + // if (isDouble) { + // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) + // } + // } return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -612,6 +763,16 @@ export async function saveBuildPack(request, reply) { return errorHandler({ status, message }) } } +export async function saveConnectedDatabase(request, reply) { + try { + const { id } = request.params + const { databaseId, type } = request.body + await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } }) + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function getSecrets(request: FastifyRequest) { try { @@ -768,7 +929,6 @@ export async function getPreviews(request: FastifyRequest) { }) } } catch ({ status, message }) { - console.log({ status, message }) return errorHandler({ status, message }) } } @@ -861,8 +1021,13 @@ export async function getBuildIdLogs(request: FastifyRequest) { orderBy: { time: 'asc' } }); const data = await prisma.build.findFirst({ where: { id: buildId } }); + const createdAt = day(data.createdAt).utc(); return { - logs, + logs: logs.map(log => { + log.time = Number(log.time) + return log + }), + took: day().diff(createdAt) / 1000, status: data?.status || 'queued' } } catch ({ status, message }) { @@ -938,3 +1103,58 @@ export async function cancelDeployment(request: FastifyRequest return errorHandler({ status, message }) } } + + +export async function createdBranchDatabase(database: any, baseDatabaseBranch: string, pullmergeRequestId: string) { + try { + if (!baseDatabaseBranch) return + const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database; + if (destinationDockerId) { + if (type === 'postgresql') { + const decryptedRootUserPassword = decrypt(rootUserPassword); + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` + }) + } + } + + + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +export async function removeBranchDatabase(database: any, pullmergeRequestId: string) { + try { + const { id, type, destinationDockerId, rootUser, rootUserPassword } = database; + if (destinationDockerId) { + if (type === 'postgresql') { + const decryptedRootUserPassword = decrypt(rootUserPassword); + // Terminate all connections to the database + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` + }) + + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` + }) + } + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index 2f698ddeb..d42906066 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -1,6 +1,6 @@ import { FastifyPluginAsync } from 'fastify'; import { OnlyId } from '../../../../types'; -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 { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; @@ -19,6 +19,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/status', async (request) => await getApplicationStatus(request)); + fastify.post('/:id/restart', async (request, reply) => await restartApplication(request, reply)); fastify.post('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); @@ -54,6 +55,8 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request)); fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply)); + fastify.post('/:id/configuration/database', async (request, reply) => await saveConnectedDatabase(request, reply)); + fastify.get('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request)); fastify.post('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 6452c0c9d..e88a79d72 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -20,15 +20,17 @@ export interface SaveApplication extends OnlyId { denoOptions: string, baseImage: string, baseBuildImage: string, - deploymentType: string + deploymentType: string, + baseDatabaseBranch: string } } export interface SaveApplicationSettings extends OnlyId { Querystring: { domain: string; }; - Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; }; + Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean }; } export interface DeleteApplication extends OnlyId { Querystring: { domain: string; }; + Body: { force: boolean } } export interface CheckDomain extends OnlyId { Querystring: { domain: string; }; diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 8372fb76d..d646a036f 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -7,7 +7,7 @@ import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, execute import { day } from '../../../../lib/dayjs'; import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; -import { SaveDatabaseType } from './types'; +import { DeleteDatabase, SaveDatabaseType } from './types'; export async function listDatabases(request: FastifyRequest) { try { @@ -167,6 +167,7 @@ export async function saveDatabaseDestination(request: FastifyRequest) { await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` }) - } catch (error) { - console.log(error); - } + } catch (error) { } try { await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); return {}; } catch (error) { - console.log(error) throw { error }; @@ -360,19 +358,22 @@ export async function getDatabaseLogs(request: FastifyRequest) return errorHandler({ status, message }) } } -export async function deleteDatabase(request: FastifyRequest) { +export async function deleteDatabase(request: FastifyRequest) { try { const teamId = request.user.teamId; const { id } = request.params; + const { force } = request.body; const database = await prisma.database.findFirst({ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, include: { destinationDocker: true, settings: true } }); - if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); - if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); - if (database.destinationDockerId) { - const everStarted = await stopDatabaseContainer(database); - if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); + if (!force) { + if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); + if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); + if (database.destinationDockerId) { + const everStarted = await stopDatabaseContainer(database); + if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); + } } await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); await prisma.database.delete({ where: { id } }); @@ -436,7 +437,7 @@ export async function saveDatabaseSettings(request: FastifyRequest => { @@ -13,7 +13,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id', async (request) => await getDatabase(request)); fastify.post('/:id', async (request, reply) => await saveDatabase(request, reply)); - fastify.delete('/:id', async (request) => await deleteDatabase(request)); + fastify.delete('/:id', async (request) => await deleteDatabase(request)); fastify.get('/:id/status', async (request) => await getDatabaseStatus(request)); diff --git a/apps/api/src/routes/api/v1/databases/types.ts b/apps/api/src/routes/api/v1/databases/types.ts index b7e4c8692..a56a45c23 100644 --- a/apps/api/src/routes/api/v1/databases/types.ts +++ b/apps/api/src/routes/api/v1/databases/types.ts @@ -2,4 +2,7 @@ import type { OnlyId } from "../../../../types"; export interface SaveDatabaseType extends OnlyId { Body: { type: string } +} +export interface DeleteDatabase extends OnlyId { + Body: { force: string } } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index 2148a1450..bb96981cf 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -30,7 +30,6 @@ export async function listDestinations(request: FastifyRequest destinations } } catch ({ status, message }) { - console.log({ status, message }) return errorHandler({ status, message }) } } @@ -114,7 +113,6 @@ export async function newDestination(request: FastifyRequest, re } } catch ({ status, message }) { - console.log({ status, message }) return errorHandler({ status, message }) } } @@ -162,7 +160,6 @@ export async function startProxy(request: FastifyRequest) { await startTraefikProxy(id); return {} } catch ({ status, message }) { - console.log({ status, message }) await stopTraefikProxy(id); return errorHandler({ status, message }) } @@ -205,23 +202,21 @@ export async function assignSSHKey(request: FastifyRequest) { return errorHandler({ status, message }) } } -export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) { +export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; await createRemoteEngineConfiguration(id); - - const { remoteIpAddress, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id } }) + const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } }) const host = `ssh://${remoteUser}@${remoteIpAddress}` 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}`); } - const { stdout:coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`); - + const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`); if (!coolifyNetwork) { await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`); } + if (isCoolifyProxyUsed) await startTraefikProxy(id); await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }) return reply.code(201).send() @@ -234,7 +229,7 @@ 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' }) + const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true }) return { isRunning } diff --git a/apps/api/src/routes/api/v1/destinations/index.ts b/apps/api/src/routes/api/v1/destinations/index.ts index 007242695..774afa285 100644 --- a/apps/api/src/routes/api/v1/destinations/index.ts +++ b/apps/api/src/routes/api/v1/destinations/index.ts @@ -23,7 +23,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request)); - fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply)); + fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply)); }; export default root; diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 037d20ffa..702ad31a8 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -65,7 +65,6 @@ export async function update(request: FastifyRequest) { ); return {}; } else { - console.log(latestVersion); await asyncSleep(2000); return {}; } @@ -78,10 +77,9 @@ export async function restartCoolify(request: FastifyRequest) { const teamId = request.user.teamId; if (teamId === '0') { if (!isDev) { - await asyncExecShell(`docker restart coolify`); + asyncExecShell(`docker restart coolify`); return {}; } else { - console.log('Restarting Coolify') return {}; } } diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 8691f2b41..c6858a7d7 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -1,15 +1,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, getServiceFromDB, getContainerUsage,isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; -import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; +import { checkContainer, isContainerExited } from '../../../../lib/docker'; import cuid from 'cuid'; import type { OnlyId } from '../../../../types'; -import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; -import { defaultServiceConfigurations } from '../../../../lib/services'; +import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; import { configureServiceType, removeService } from '../../../../lib/services/common'; @@ -269,7 +267,6 @@ export async function saveService(request: FastifyRequest, reply: F if (exposePort) exposePort = Number(exposePort); type = fixType(type) - const update = saveUpdateableFields(type, request.body[type]) const data = { fqdn, @@ -400,17 +397,33 @@ export async function deleteServiceStorage(request: FastifyRequest, reply: FastifyReply) { +export async function setSettingsService(request: FastifyRequest, reply: FastifyReply) { try { const { type } = request.params if (type === 'wordpress') { return await setWordpressSettings(request, reply) } + if (type === 'glitchtip') { + return await setGlitchTipSettings(request, reply) + } throw `Service type ${type} not supported.` } catch ({ status, message }) { return errorHandler({ status, message }) } } +async function setGlitchTipSettings(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params + const { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls } = request.body + await prisma.glitchTip.update({ + where: { serviceId: id }, + data: { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls } + }); + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} async function setWordpressSettings(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params @@ -547,10 +560,7 @@ export async function activateWordpressFtp(request: FastifyRequest => { @@ -71,7 +71,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id/:type/start', async (request) => await startService(request)); fastify.post('/:id/:type/stop', async (request) => await stopService(request)); - fastify.post('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); + fastify.post('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); fastify.post('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(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 f09b4423f..3de06fa57 100644 --- a/apps/api/src/routes/api/v1/services/types.ts +++ b/apps/api/src/routes/api/v1/services/types.ts @@ -89,6 +89,10 @@ export interface ActivateWordpressFtp extends OnlyId { } } - - - +export interface SetGlitchTipSettings extends OnlyId { + Body: { + enableOpenUserRegistration: boolean, + emailSmtpUseSsl: boolean, + emailSmtpUseTls: boolean + } +} diff --git a/apps/api/src/routes/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index 106311c5c..de288022d 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -3,8 +3,7 @@ import cuid from "cuid"; import crypto from "crypto"; import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common"; import { checkContainer, removeContainer } from "../../../lib/docker"; -import { scheduler } from "../../../lib/scheduler"; -import { getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; +import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers"; import type { FastifyReply, FastifyRequest } from "fastify"; import type { GitHubEvents, InstallGithub } from "./types"; @@ -67,7 +66,6 @@ export async function configureGitHubApp(request, reply) { } export async function gitHubEvents(request: FastifyRequest): Promise { try { - const buildId = cuid(); const allowedGithubEvents = ['push', 'pull_request']; const allowedActions = ['opened', 'reopened', 'synchronize', 'closed']; const githubEvent = request.headers['x-github-event']?.toString().toLowerCase(); @@ -87,126 +85,139 @@ export async function gitHubEvents(request: FastifyRequest): Promi 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; - //@ts-ignore - const hmac = crypto.createHmac('sha256', webhookSecret); - const digest = Buffer.from( - 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'), - 'utf8' - ); - if (!isDev) { - const checksum = Buffer.from(githubSignature, 'utf8'); + const applicationsFound = await getApplicationFromDBWebhook(projectId, branch); + if (applicationsFound && applicationsFound.length > 0) { + for (const application of applicationsFound) { + const buildId = cuid(); + const webhookSecret = application.gitSource.githubApp.webhookSecret || null; //@ts-ignore - if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { - throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' } - }; - } - - - if (githubEvent === 'push') { - if (!applicationFound.configHash) { - const configHash = crypto - //@ts-ignore - .createHash('sha256') - .update( - JSON.stringify({ - buildPack: applicationFound.buildPack, - port: applicationFound.port, - exposePort: applicationFound.exposePort, - installCommand: applicationFound.installCommand, - buildCommand: applicationFound.buildCommand, - startCommand: applicationFound.startCommand - }) - ) - .digest('hex'); - await prisma.application.updateMany({ - where: { branch, projectId }, - data: { configHash } - }); - } - await prisma.application.update({ - where: { id: applicationFound.id }, - data: { updatedAt: new Date() } - }); - await prisma.build.create({ - data: { - id: buildId, - applicationId: applicationFound.id, - destinationDockerId: applicationFound.destinationDocker.id, - gitSourceId: applicationFound.gitSource.id, - githubAppId: applicationFound.gitSource.githubApp?.id, - gitlabAppId: applicationFound.gitSource.gitlabApp?.id, - status: 'queued', - type: 'webhook_commit' - } - }); - return { - message: 'Queued. Thank you!' - }; - } else if (githubEvent === 'pull_request') { - const pullmergeRequestId = body.number.toString(); - const pullmergeRequestAction = body.action; - 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.' } + const hmac = crypto.createHmac('sha256', webhookSecret); + const digest = Buffer.from( + 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'), + 'utf8' + ); + if (!isDev) { + const checksum = Buffer.from(githubSignature, 'utf8'); + //@ts-ignore + if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { + throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' } + }; } - if (applicationFound.settings.previews) { - if (applicationFound.destinationDockerId) { - const isRunning = await checkContainer( - { - dockerId: applicationFound.destinationDocker.id, - container: applicationFound.id - } - ); - if (!isRunning) { - throw { status: 500, message: 'Application not running.' } - } - } - if ( - pullmergeRequestAction === 'opened' || - pullmergeRequestAction === 'reopened' || - pullmergeRequestAction === 'synchronize' - ) { + if (githubEvent === 'push') { + if (!application.configHash) { + const configHash = crypto + //@ts-ignore + .createHash('sha256') + .update( + JSON.stringify({ + buildPack: application.buildPack, + port: application.port, + exposePort: application.exposePort, + installCommand: application.installCommand, + buildCommand: application.buildCommand, + startCommand: application.startCommand + }) + ) + .digest('hex'); await prisma.application.update({ - where: { id: applicationFound.id }, - data: { updatedAt: new Date() } + where: { id: application.id }, + data: { configHash } }); - await prisma.build.create({ - data: { - id: buildId, - pullmergeRequestId, - sourceBranch, - applicationId: applicationFound.id, - destinationDockerId: applicationFound.destinationDocker.id, - gitSourceId: applicationFound.gitSource.id, - githubAppId: applicationFound.gitSource.githubApp?.id, - gitlabAppId: applicationFound.gitSource.gitlabApp?.id, - status: 'queued', - type: 'webhook_pr' - } - }); - - return { - message: 'Queued. Thank you!' - }; - } else if (pullmergeRequestAction === 'closed') { - if (applicationFound.destinationDockerId) { - const id = `${applicationFound.id}-${pullmergeRequestId}`; - await removeContainer({ id, dockerId: applicationFound.destinationDocker.id }); - } - return { - message: 'Removed preview. Thank you!' - }; } - } else { - throw { status: 500, message: 'Pull request previews are not enabled.' } + + await prisma.application.update({ + where: { id: application.id }, + data: { updatedAt: new Date() } + }); + await prisma.build.create({ + data: { + id: buildId, + applicationId: application.id, + destinationDockerId: application.destinationDocker.id, + gitSourceId: application.gitSource.id, + githubAppId: application.gitSource.githubApp?.id, + gitlabAppId: application.gitSource.gitlabApp?.id, + status: 'queued', + type: 'webhook_commit' + } + }); + console.log(`Webhook for ${application.name} queued.`) + + } else if (githubEvent === 'pull_request') { + const pullmergeRequestId = body.number.toString(); + const pullmergeRequestAction = body.action; + 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.' } + } + + if (application.settings.previews) { + if (application.destinationDockerId) { + const isRunning = await checkContainer( + { + dockerId: application.destinationDocker.id, + container: application.id + } + ); + if (!isRunning) { + throw { status: 500, message: 'Application not running.' } + } + } + if ( + pullmergeRequestAction === 'opened' || + pullmergeRequestAction === 'reopened' || + pullmergeRequestAction === 'synchronize' + ) { + await prisma.application.update({ + where: { id: application.id }, + data: { updatedAt: new Date() } + }); + if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') { + // Coolify hosted database + if (application.connectedDatabase.databaseId) { + const databaseId = application.connectedDatabase.databaseId; + const database = await prisma.database.findUnique({ where: { id: databaseId } }); + if (database) { + await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId); + } + } + } + await prisma.build.create({ + data: { + id: buildId, + pullmergeRequestId, + sourceBranch, + applicationId: application.id, + destinationDockerId: application.destinationDocker.id, + gitSourceId: application.gitSource.id, + githubAppId: application.gitSource.githubApp?.id, + gitlabAppId: application.gitSource.gitlabApp?.id, + status: 'queued', + type: 'webhook_pr' + } + }); + + + } else if (pullmergeRequestAction === 'closed') { + if (application.destinationDockerId) { + const id = `${application.id}-${pullmergeRequestId}`; + try { + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } catch (error) { } + } + if (application.connectedDatabase.databaseId) { + const databaseId = application.connectedDatabase.databaseId; + const database = await prisma.database.findUnique({ where: { id: databaseId } }); + if (database) { + await removeBranchDatabase(database, pullmergeRequestId); + } + } + } + } } } } - throw { status: 500, message: 'Not handled event.' } } catch ({ status, message }) { return errorHandler({ status, message }) } diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index 52512c5fc..e661a29c5 100644 --- a/apps/api/src/routes/webhooks/gitlab/handlers.ts +++ b/apps/api/src/routes/webhooks/gitlab/handlers.ts @@ -2,9 +2,8 @@ import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; import type { FastifyReply, FastifyRequest } from "fastify"; -import { errorHandler, getAPIUrl, isDev, listSettings, prisma } from "../../../lib/common"; +import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common"; import { checkContainer, removeContainer } from "../../../lib/docker"; -import { scheduler } from "../../../lib/scheduler"; import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; import type { ConfigureGitLabApp, GitLabEvents } from "./types"; @@ -30,7 +29,7 @@ export async function configureGitLabApp(request: FastifyRequest) { const { object_kind: objectKind, ref, project_id } = request.body try { - const buildId = cuid(); const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const webhookToken = request.headers['x-gitlab-token']; - if (!webhookToken) { + if (!webhookToken && !isDev) { throw { status: 500, message: 'Invalid webhookToken.' } } if (objectKind === 'push') { const projectId = Number(project_id); const branch = ref.split('/')[2]; - const applicationFound = await getApplicationFromDBWebhook(projectId, branch); - if (applicationFound) { - if (!applicationFound.configHash) { - const configHash = crypto - .createHash('sha256') - .update( - JSON.stringify({ - buildPack: applicationFound.buildPack, - port: applicationFound.port, - exposePort: applicationFound.exposePort, - installCommand: applicationFound.installCommand, - buildCommand: applicationFound.buildCommand, - startCommand: applicationFound.startCommand - }) - ) - .digest('hex'); - await prisma.application.updateMany({ - where: { branch, projectId }, - data: { configHash } + const applicationsFound = await getApplicationFromDBWebhook(projectId, branch); + if (applicationsFound && applicationsFound.length > 0) { + for (const application of applicationsFound) { + const buildId = cuid(); + if (!application.configHash) { + const configHash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + buildPack: application.buildPack, + port: application.port, + exposePort: application.exposePort, + installCommand: application.installCommand, + buildCommand: application.buildCommand, + startCommand: application.startCommand + }) + ) + .digest('hex'); + await prisma.application.update({ + where: { id: application.id }, + data: { configHash } + }); + } + await prisma.application.update({ + where: { id: application.id }, + data: { updatedAt: new Date() } + }); + await prisma.build.create({ + data: { + id: buildId, + applicationId: application.id, + destinationDockerId: application.destinationDocker.id, + gitSourceId: application.gitSource.id, + githubAppId: application.gitSource.githubApp?.id, + gitlabAppId: application.gitSource.gitlabApp?.id, + status: 'queued', + type: 'webhook_commit' + } }); } - await prisma.application.update({ - where: { id: applicationFound.id }, - data: { updatedAt: new Date() } - }); - await prisma.build.create({ - data: { - id: buildId, - applicationId: applicationFound.id, - destinationDockerId: applicationFound.destinationDocker.id, - gitSourceId: applicationFound.gitSource.id, - githubAppId: applicationFound.gitSource.githubApp?.id, - gitlabAppId: applicationFound.gitSource.gitlabApp?.id, - status: 'queued', - type: 'webhook_commit' - } - }); - - return { - message: 'Queued. Thank you!' - }; - } } else if (objectKind === 'merge_request') { const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body @@ -105,64 +101,63 @@ export async function gitLabEvents(request: FastifyRequest) { throw { status: 500, message: 'Draft MR, do nothing.' } } - const applicationFound = await getApplicationFromDBWebhook(projectId, targetBranch); - if (applicationFound) { - if (applicationFound.settings.previews) { - if (applicationFound.destinationDockerId) { - const isRunning = await checkContainer( - { - dockerId: applicationFound.destinationDocker.id, - container: applicationFound.id + const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch); + if (applicationsFound && applicationsFound.length > 0) { + for (const application of applicationsFound) { + const buildId = cuid(); + if (application.settings.previews) { + if (application.destinationDockerId) { + const isRunning = await checkContainer( + { + dockerId: application.destinationDocker.id, + container: application.id + } + ); + if (!isRunning) { + throw { status: 500, message: 'Application not running.' } } - ); - if (!isRunning) { - throw { status: 500, message: 'Application not running.' } } - } - if (!isDev && applicationFound.gitSource.gitlabApp.webhookToken !== webhookToken) { - throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' } - } - if ( - action === 'opened' || - action === 'reopen' || - action === 'open' || - action === 'update' - ) { - await prisma.application.update({ - where: { id: applicationFound.id }, - data: { updatedAt: new Date() } - }); - await prisma.build.create({ - data: { - id: buildId, - pullmergeRequestId, - sourceBranch, - applicationId: applicationFound.id, - destinationDockerId: applicationFound.destinationDocker.id, - gitSourceId: applicationFound.gitSource.id, - githubAppId: applicationFound.gitSource.githubApp?.id, - gitlabAppId: applicationFound.gitSource.gitlabApp?.id, - status: 'queued', - type: 'webhook_mr' + if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) { + throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' } + } + if ( + action === 'opened' || + action === 'reopen' || + action === 'open' || + action === 'update' + ) { + await prisma.application.update({ + where: { id: application.id }, + data: { updatedAt: new Date() } + }); + await prisma.build.create({ + data: { + id: buildId, + pullmergeRequestId, + sourceBranch, + applicationId: application.id, + destinationDockerId: application.destinationDocker.id, + gitSourceId: application.gitSource.id, + githubAppId: application.gitSource.githubApp?.id, + gitlabAppId: application.gitSource.gitlabApp?.id, + status: 'queued', + type: 'webhook_mr' + } + }); + return { + message: 'Queued. Thank you!' + }; + } else if (action === 'close') { + if (application.destinationDockerId) { + const id = `${application.id}-${pullmergeRequestId}`; + await removeContainer({ id, dockerId: application.destinationDocker.id }); } - }); - return { - message: 'Queued. Thank you!' - }; - } else if (action === 'close') { - if (applicationFound.destinationDockerId) { - const id = `${applicationFound.id}-${pullmergeRequestId}`; - await removeContainer({ id, dockerId: applicationFound.destinationDocker.id }); + } - return { - message: 'Removed preview. Thank you!' - }; } } - throw { status: 500, message: 'Merge request previews are not enabled.' } } } - throw { status: 500, message: 'Not handled event.' } } catch ({ status, message }) { return errorHandler({ status, message }) } diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index ec6adc397..3e2cd5952 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -3,6 +3,7 @@ import { errorHandler, getDomain, isDev, prisma, executeDockerCmd } from "../../ import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; import { includeServices } from "../../../lib/services/common"; import { TraefikOtherConfiguration } from "./types"; +import { OnlyId } from "../../../types"; function configureMiddleware( { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, @@ -25,7 +26,30 @@ function configureMiddleware( ] } }; + if (type === 'appwrite') { + traefik.http.routers[`${id}-realtime`] = { + entrypoints: ['websecure'], + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`, + service: `${`${id}-realtime`}`, + tls: { + domains: { + main: `${domain}` + } + }, + middlewares: [] + }; + + traefik.http.services[`${id}-realtime`] = { + loadbalancer: { + servers: [ + { + url: `http://${container}-realtime:${port}` + } + ] + } + }; + } if (isDualCerts) { traefik.http.routers[`${id}-secure`] = { entrypoints: ['websecure'], @@ -112,6 +136,23 @@ function configureMiddleware( ] } }; + if (type === 'appwrite') { + traefik.http.routers[`${id}-realtime`] = { + entrypoints: ['web'], + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/v1/realtime\`)`, + service: `${id}-realtime`, + middlewares: [] + }; + traefik.http.services[`${id}-realtime`] = { + loadbalancer: { + servers: [ + { + url: `http://${container}-realtime:${port}` + } + ] + } + }; + } if (!isDualCerts) { if (isWWW) { @@ -490,7 +531,7 @@ export async function traefikOtherConfiguration(request: FastifyRequest) { const { id } = request.params try { const traefik = { diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index 6367fd566..71f3db158 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -36,4 +36,3 @@ export interface SaveDatabaseSettings extends OnlyId { } - diff --git a/apps/i18n/.env.example b/apps/i18n/.env.example new file mode 100644 index 000000000..8bdf45a64 --- /dev/null +++ b/apps/i18n/.env.example @@ -0,0 +1,4 @@ +WEBLATE_INSTANCE_URL=http://localhost +WEBLATE_COMPONENT_NAME=coolify +WEBLATE_TOKEN= +TRANSLATION_DIR= \ No newline at end of file diff --git a/apps/i18n/.gitignore b/apps/i18n/.gitignore new file mode 100644 index 000000000..df67586b0 --- /dev/null +++ b/apps/i18n/.gitignore @@ -0,0 +1 @@ +locales/* \ No newline at end of file diff --git a/apps/i18n/index.mjs b/apps/i18n/index.mjs new file mode 100644 index 000000000..85e146073 --- /dev/null +++ b/apps/i18n/index.mjs @@ -0,0 +1,63 @@ +import dotenv from 'dotenv'; +dotenv.config() +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url'; +import Gettext from 'node-gettext' +import { po } from 'gettext-parser' +import got from 'got'; +const __filename = fileURLToPath(import.meta.url); + +const __dirname = path.dirname(__filename); + +const weblateInstanceURL = process.env.WEBLATE_INSTANCE_URL; +const weblateComponentName = process.env.WEBLATE_COMPONENT_NAME +const token = process.env.WEBLATE_TOKEN; + +const translationsDir = process.env.TRANSLATION_DIR; +const translationsPODir = './locales'; +const locales = [] +const domain = 'locale' + +const translations = await got(`${weblateInstanceURL}/api/components/${weblateComponentName}/glossary/translations/?format=json`, { + headers: { + "Authorization": `Token ${token}` + } +}).json() +for (const translation of translations.results) { + const code = translation.language_code + locales.push(code) + + const fileUrl = translation.file_url.replace('=json', '=po') + const file = await got(fileUrl, { + headers: { + "Authorization": `Token ${token}` + } + }).text() + fs.writeFileSync(path.join(__dirname, translationsPODir, domain + '-' + code + '.po'), file) +} + + +const gt = new Gettext() + +locales.forEach((locale) => { + let json = {} + const fileName = `${domain}-${locale}.po` + const translationsFilePath = path.join(translationsPODir, fileName) + const translationsContent = fs.readFileSync(translationsFilePath) + + const parsedTranslations = po.parse(translationsContent) + const a = gt.gettext(parsedTranslations) + for (const [key, value] of Object.entries(a)) { + if (key === 'translations') { + for (const [key1, value1] of Object.entries(value)) { + if (key1 !== '') { + for (const [key2, value2] of Object.entries(value1)) { + json[value2.msgctxt] = value2.msgstr[0] + } + } + } + } + } + fs.writeFileSync(`${translationsDir}/${locale}.json`, JSON.stringify(json)) +}) \ No newline at end of file diff --git a/apps/i18n/package.json b/apps/i18n/package.json new file mode 100644 index 000000000..bb4534514 --- /dev/null +++ b/apps/i18n/package.json @@ -0,0 +1,15 @@ +{ + "name": "i18n-converter", + "description": "Convert Weblate translations to sveltekit-i18n", + "license": "Apache-2.0", + "scripts": { + "translate": "node index.mjs" + }, + "type": "module", + "dependencies": { + "node-gettext": "3.0.0", + "gettext-parser": "6.0.0", + "got": "12.3.1", + "dotenv": "16.0.2" + } +} \ No newline at end of file diff --git a/apps/ui/package.json b/apps/ui/package.json index cc5fe9009..caf05c233 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -14,33 +14,38 @@ "format": "prettier --write --plugin-search-dir=. ." }, "devDependencies": { + "@floating-ui/dom": "1.0.1", "@playwright/test": "1.25.1", + "@popperjs/core": "2.11.6", "@sveltejs/kit": "1.0.0-next.405", "@types/js-cookie": "3.0.2", - "@typescript-eslint/eslint-plugin": "5.35.1", - "@typescript-eslint/parser": "5.35.1", + "@typescript-eslint/eslint-plugin": "5.36.1", + "@typescript-eslint/parser": "5.36.1", "autoprefixer": "10.4.8", - "eslint": "8.22.0", + "classnames": "2.3.1", + "eslint": "8.23.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-svelte3": "4.0.0", + "flowbite": "1.5.2", + "flowbite-svelte": "0.26.2", "postcss": "8.4.16", "prettier": "2.7.1", "prettier-plugin-svelte": "2.7.0", - "svelte": "3.49.0", - "svelte-check": "2.8.1", + "svelte": "3.50.0", + "svelte-check": "2.9.0", "svelte-preprocess": "4.10.7", "tailwindcss": "3.1.8", "tailwindcss-scrollbar": "0.1.0", "tslib": "2.4.0", - "typescript": "4.7.4", - "vite": "3.0.5" + "typescript": "4.8.2", + "vite": "3.1.0" }, "type": "module", "dependencies": { "@sveltejs/adapter-static": "1.0.0-next.39", - "@tailwindcss/typography": "^0.5.4", + "@tailwindcss/typography": "^0.5.7", "cuid": "2.1.8", - "daisyui": "2.24.0", + "daisyui": "2.24.2", "js-cookie": "3.0.1", "p-limit": "4.0.0", "svelte-select": "4.4.7", diff --git a/apps/ui/src/lib/components/DocLink.svelte b/apps/ui/src/lib/components/DocLink.svelte new file mode 100644 index 000000000..0646734be --- /dev/null +++ b/apps/ui/src/lib/components/DocLink.svelte @@ -0,0 +1,32 @@ + + + + + + + + + + +See details in the documentation diff --git a/apps/ui/src/lib/components/Explainer.svelte b/apps/ui/src/lib/components/Explainer.svelte index 813220c67..e51934a58 100644 --- a/apps/ui/src/lib/components/Explainer.svelte +++ b/apps/ui/src/lib/components/Explainer.svelte @@ -1,6 +1,31 @@ -
{@html text}
+
+ + +
+{#if id} + {@html explanation} +{/if} diff --git a/apps/ui/src/lib/components/Setting.svelte b/apps/ui/src/lib/components/Setting.svelte index 30f45755b..b8aafbba8 100644 --- a/apps/ui/src/lib/components/Setting.svelte +++ b/apps/ui/src/lib/components/Setting.svelte @@ -1,6 +1,8 @@
-
{title}
- +
+ {title} +
-
+
Use setting
+ +{#if dataTooltip} + {dataTooltip} +{/if} diff --git a/apps/ui/src/lib/components/SimpleExplainer.svelte b/apps/ui/src/lib/components/SimpleExplainer.svelte new file mode 100644 index 000000000..6a3198c27 --- /dev/null +++ b/apps/ui/src/lib/components/SimpleExplainer.svelte @@ -0,0 +1,6 @@ + + +
{@html text}
\ No newline at end of file diff --git a/apps/ui/src/lib/components/Tooltip.svelte b/apps/ui/src/lib/components/Tooltip.svelte new file mode 100644 index 000000000..c20e4836d --- /dev/null +++ b/apps/ui/src/lib/components/Tooltip.svelte @@ -0,0 +1,8 @@ + + + diff --git a/apps/ui/src/lib/components/UpdateAvailable.svelte b/apps/ui/src/lib/components/UpdateAvailable.svelte index 4122aae3e..db98d8699 100644 --- a/apps/ui/src/lib/components/UpdateAvailable.svelte +++ b/apps/ui/src/lib/components/UpdateAvailable.svelte @@ -16,7 +16,6 @@ updateStatus.loading = true; try { if (dev) { - console.log(`updating to ${latestVersion}`); await asyncSleep(4000); return window.location.reload(); } else { diff --git a/apps/ui/src/lib/components/Usage.svelte b/apps/ui/src/lib/components/Usage.svelte index 08e63e455..e5437a1d5 100644 --- a/apps/ui/src/lib/components/Usage.svelte +++ b/apps/ui/src/lib/components/Usage.svelte @@ -26,7 +26,7 @@ import { addToast, appSession } from '$lib/store'; import { onDestroy, onMount } from 'svelte'; import { get, post } from '$lib/api'; - import { errorNotification } from '$lib/common'; + import { asyncSleep, errorNotification } from '$lib/common'; async function getStatus() { if (loading.usage) return; loading.usage = true; @@ -42,6 +42,26 @@ loading.restart = true; try { await post(`/internal/restart`, {}); + await asyncSleep(10000); + let reachable = false; + let tries = 0; + do { + await asyncSleep(4000); + try { + await get(`/undead`); + reachable = true; + } catch (error) { + reachable = false; + } + if (reachable) break; + tries++; + } while (!reachable || tries < 120); + addToast({ + message: 'New version reachable. Reloading...', + type: 'success' + }); + await asyncSleep(3000); + return window.location.reload(); addToast({ type: 'success', message: 'Coolify restarted successfully. It will take a moment.' @@ -89,10 +109,8 @@

Hardware Details

{#if $appSession.teamId === '0'} - Cleanup Storage - {:else if $status.application.isRunning} - -
handleDeploySubmit(true)}> - -
- {:else} -
handleDeploySubmit(false)}> - -
- {/if} + + + + + + Open
+ {/if} + + {#if $status.application.isExited} - + + + + + + Application exited with an error! + {/if} + {#if $status.application.initialLoading} + + + + + + + + + + {:else if $status.application.isRunning} + - {#if !application.settings.isBot} - - - {/if} -
- - - - -
+ + + + + + Stop + Restart (useful to change secrets) + +
handleDeploySubmit(true)}> + + Force redeploy (without cache) +
+ {:else} +
handleDeploySubmit(false)}> + + Deploy +
+ {/if} + +
+ + + + Configurations + + + Secrets + + + Persistent Storages + {#if !application.settings.isBot} + + + Previews + {/if} +
+ + + Application Logs + + + Build Logs +
+ + {#if forceDelete} + + Force Delete + {:else} + + Delete {/if} diff --git a/apps/ui/src/routes/applications/[id]/configuration/_GithubRepositories.svelte b/apps/ui/src/routes/applications/[id]/configuration/_GithubRepositories.svelte index 95a307ef7..922c2b174 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_GithubRepositories.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_GithubRepositories.svelte @@ -95,19 +95,19 @@ async function isBranchAlreadyUsed(event: any) { selected.branch = event.detail.value; try { - const data = await get( - `/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}` - ); - if (data.used) { - const sure = confirm($t('application.configuration.branch_already_in_use')); - if (sure) { - selected.autodeploy = false; - showSave = true; - return true; - } - showSave = false; - return true; - } + // const data = await get( + // `/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}` + // ); + // if (data.used) { + // const sure = confirm($t('application.configuration.branch_already_in_use')); + // if (sure) { + // selected.autodeploy = false; + // showSave = true; + // return true; + // } + // showSave = false; + // return true; + // } showSave = true; } catch (error) { showSave = false; diff --git a/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte b/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte index 02fa103bc..3da8c6415 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte @@ -169,10 +169,6 @@ } } } - function selectBranch(event: any) { - selected.branch = event.detail; - isBranchAlreadyUsed(); - } async function loadBranches(page: number = 1) { let perPage = 100; //@ts-ignore @@ -199,21 +195,22 @@ } } - async function isBranchAlreadyUsed() { + async function isBranchAlreadyUsed(event) { + selected.branch = event.detail; try { - const data = await get( - `/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` - ); - if (data.used) { - const sure = confirm($t('application.configuration.branch_already_in_use')); - if (sure) { - autodeploy = false; - showSave = true; - return true; - } - showSave = false; - return true; - } + // const data = await get( + // `/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` + // ); + // if (data.used) { + // const sure = confirm($t('application.configuration.branch_already_in_use')); + // if (sure) { + // autodeploy = false; + // showSave = true; + // return true; + // } + // showSave = false; + // return true; + // } showSave = true; } catch (error) { return errorNotification(error); @@ -227,9 +224,7 @@ } } async function setWebhook(url: any, webhookToken: any) { - const host = dev - ? getWebhookUrl('gitlab') - : `${window.location.origin}/webhooks/gitlab/events`; + const host = dev ? getWebhookUrl('gitlab') : `${window.location.origin}/webhooks/gitlab/events`; try { await post( url, @@ -294,17 +289,15 @@ ); await post(updateDeployKeyIdUrl, { deployKeyId: id }); } catch (error) { - return errorNotification(error); - } finally { loading.save = false; + return errorNotification(error); } try { await setWebhook(webhookUrl, webhookToken); } catch (error) { - return errorNotification(error); - } finally { loading.save = false; + return errorNotification(error); } const url = `/applications/${id}/configuration/repository`; @@ -317,11 +310,11 @@ autodeploy, webhookToken }); + loading.save = false; return await goto(from || `/applications/${id}/configuration/buildpack`); } catch (error) { - return errorNotification(error); - } finally { loading.save = false; + return errorNotification(error); } } async function handleSubmit() { @@ -396,7 +389,7 @@ showIndicator={!loading.branches} isWaiting={loading.branches} isDisabled={loading.branches || !selected.project} - on:select={selectBranch} + on:select={isBranchAlreadyUsed} on:clear={() => { showSave = false; selected.branch = null; @@ -425,7 +418,7 @@ configuration here.
- +
diff --git a/apps/ui/src/routes/applications/[id]/configuration/database.svelte b/apps/ui/src/routes/applications/[id]/configuration/database.svelte new file mode 100644 index 000000000..919682b00 --- /dev/null +++ b/apps/ui/src/routes/applications/[id]/configuration/database.svelte @@ -0,0 +1,162 @@ + + + + +
+
Select a Database
+
+ +
+ {#if !databases || ownDatabases.length === 0} +
+
{$t('database.no_databases_found')}
+
+ {/if} + {#if ownDatabases.length > 0 || otherDatabases.length > 0} +
+
+ {#each ownDatabases as database} + + {/each} +
+ {#if otherDatabases.length > 0 && $appSession.teamId === '0'} +
Other Databases
+ + {/if} +
+ {/if} + +
+
+
Connect a Hosted / Remote Database
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
diff --git a/apps/ui/src/routes/applications/[id]/configuration/source.svelte b/apps/ui/src/routes/applications/[id]/configuration/source.svelte index 6c02f35e1..d1252c74f 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/source.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/source.svelte @@ -32,7 +32,7 @@ import { errorNotification } from '$lib/common'; import { appSession } from '$lib/store'; import PublicRepository from './_PublicRepository.svelte'; -import Explainer from '$lib/components/Explainer.svelte'; + import DocLink from '$lib/components/DocLink.svelte'; const { id } = $page.params; const from = $page.url.searchParams.get('from'); @@ -192,7 +192,9 @@ import Explainer from '$lib/components/Explainer.svelte';
{/if}
-
Public Repository
- - +
+
Public Repository
+ +
+
diff --git a/apps/ui/src/routes/applications/[id]/index.svelte b/apps/ui/src/routes/applications/[id]/index.svelte index 283fb1ab0..f11286b98 100644 --- a/apps/ui/src/routes/applications/[id]/index.svelte +++ b/apps/ui/src/routes/applications/[id]/index.svelte @@ -31,15 +31,24 @@ import { page } from '$app/stores'; import { onDestroy, onMount } from 'svelte'; import Select from 'svelte-select'; - - import Explainer from '$lib/components/Explainer.svelte'; import { get, post } from '$lib/api'; import cuid from 'cuid'; - import { browser } from '$app/env'; - import { addToast, appSession, disabledButton, setLocation, status } from '$lib/store'; + import { + addToast, + appSession, + checkIfDeploymentEnabledApplications, + setLocation, + status, + isDeploymentEnabled, + features + } from '$lib/store'; import { t } from '$lib/translations'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; - import Setting from './_Setting.svelte'; + import Setting from '$lib/components/Setting.svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; + import Explainer from '$lib/components/Explainer.svelte'; + import { goto } from '$app/navigation'; + const { id } = $page.params; $: isDisabled = @@ -63,7 +72,9 @@ let dualCerts = application.settings.dualCerts; let autodeploy = application.settings.autodeploy; let isBot = application.settings.isBot; + let isDBBranching = application.settings.isDBBranching; + let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let isNonWWWDomainOK = false; let isWWWDomainOK = false; @@ -161,6 +172,9 @@ application.settings.isBot = isBot; setLocation(application, settings); } + if (name === 'isDBBranching') { + isDBBranching = !isDBBranching; + } try { await post(`/applications/${id}/settings`, { previews, @@ -168,6 +182,7 @@ dualCerts, isBot, autodeploy, + isDBBranching, branch: application.branch, projectId: application.projectId }); @@ -191,14 +206,19 @@ if (name === 'isBot') { isBot = !isBot; } + if (name === 'isDBBranching') { + isDBBranching = !isDBBranching; + } return errorNotification(error); + } finally { + $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); } } async function handleSubmit() { - if (loading || (!application.fqdn && !isBot)) return; + if (loading) return; loading = true; try { - nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); + nonWWWDomain = application.fqdn != null && getDomain(application.fqdn).replace(/^www\./, ''); if (application.deploymentType) application.deploymentType = application.deploymentType.toLowerCase(); !isBot && @@ -208,16 +228,17 @@ dualCerts, exposePort: application.exposePort })); - await post(`/applications/${id}`, { ...application }); + await post(`/applications/${id}`, { ...application, baseDatabaseBranch }); setLocation(application, settings); - $disabledButton = false; + $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); + forceSave = false; + addToast({ message: 'Configuration saved.', type: 'success' }); } catch (error) { - console.log(error); //@ts-ignore if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) { forceSave = true; @@ -281,6 +302,7 @@
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {/if} + Open on Git {/if}
@@ -426,7 +449,7 @@ > {/if} -
+
@@ -440,10 +463,15 @@
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} -
+
+ >{$t('application.base_build_image')} + +
-
{/if} {#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
Deployment Type +
Static is for static websites, node is for server-side applications."} + />
+
+
+ Connected to {application.connectedDatabase.databaseId} +
+ {/if} + {/if} + {/if} + {/if}
{$t('application.application')}
@@ -513,35 +576,41 @@
changeSettings('isBot')} title="Is your application a bot?" - description="You can deploy applications without domains.
You can also make them to listen on IP:EXPOSEDPORT as well.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming connection." + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." disabled={$status.application.isRunning} />
+
+ !$status.application.isRunning && changeSettings('dualCerts')} + /> +
{#if !isBot} -
-
- - {#if browser && window.location.hostname === 'demo.coolify.io'} - - {/if} - -
+
+
-
- !$status.application.isRunning && changeSettings('dualCerts')} - /> -
{/if} {#if application.buildPack === 'python'}
@@ -645,7 +703,10 @@ {/if} {#if !staticDeployments.includes(application.buildPack)}
- + -
{/if} -
- +
+ -
Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} - />
{#if !notNodeDeployments.includes(application.buildPack)} -
+
@@ -698,7 +759,7 @@ placeholder="{$t('forms.default')}: yarn build" />
-
+
@@ -715,7 +776,9 @@ {#if application.buildPack === 'docker'}
Dockerfile Location /data/Dockerfile or /Dockerfile."} + /> -
{/if} {#if application.buildPack === 'deno'} @@ -743,7 +803,11 @@ />
- + -
{/if} {#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}
{$t('forms.base_directory')} + Could be useful with monorepos."} + /> -
{$t('forms.publish_directory')} + For example: dist,_site or public."} + /> -
changeSettings('autodeploy')} @@ -812,9 +878,10 @@ />
{/if} - {#if !application.settings.isBot} + {#if !application.settings.isBot && !application.settings.isPublicRepository}
changeSettings('previews')} @@ -825,6 +892,7 @@ {/if}
changeSettings('debug')} diff --git a/apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte b/apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte index 206cdfc5a..41354eefc 100644 --- a/apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/_BuildLog.svelte @@ -6,14 +6,13 @@ import { page } from '$app/stores'; - import Loading from '$lib/components/Loading.svelte'; import { get, post } from '$lib/api'; import { t } from '$lib/translations'; import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import { errorNotification } from '$lib/common'; + import Tooltip from '$lib/components/Tooltip.svelte'; let logs: any = []; - let loading = true; let currentStatus: any; let streamInterval: any; let followingBuild: any; @@ -46,7 +45,6 @@ logs = logs.concat( responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) ); - loading = false; streamInterval = setInterval(async () => { if (status !== 'running' && status !== 'queued') { clearInterval(streamInterval); @@ -63,13 +61,12 @@ logs = logs.concat( data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) ); - dispatch('updateBuildStatus', { status }); + dispatch('updateBuildStatus', { status, took: data.took }); } catch (error) { return errorNotification(error); } }, 1000); } catch (error) { - console.log(error); return errorNotification(error); } } @@ -82,7 +79,6 @@ applicationId: id }); } catch (error) { - console.log(error); return errorNotification(error); } } @@ -96,84 +92,82 @@ }); -{#if loading} - -{:else} -
- {#if currentStatus === 'running'} - - {/if} - {#if currentStatus === 'queued'} -
{$t('application.build.queued_waiting_exec')}
- {:else} -
+
+ {#if currentStatus === 'running'} + + {/if} + {#if currentStatus === 'queued'} +
{$t('application.build.queued_waiting_exec')}
+ {:else} +
+ + Follow Logs + {#if currentStatus === 'running'} - {#if currentStatus === 'running'} - - {/if} -
- {#if logs.length > 0} -
- {#each logs as log} -
{log.line + '\n'}
- {/each} -
- {:else} -
- No logs found. -
+ Cancel build {/if} +
+ {#if logs.length > 0} +
+ {#each logs as log} +
{log.line + '\n'}
+ {/each} +
+ {:else} +
+ No logs found. +
{/if} -
-{/if} + {/if} +
diff --git a/apps/ui/src/routes/applications/[id]/logs/build.svelte b/apps/ui/src/routes/applications/[id]/logs/build.svelte index d1373e71e..faeec72c0 100644 --- a/apps/ui/src/routes/applications/[id]/logs/build.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/build.svelte @@ -28,17 +28,20 @@ import { get } from '$lib/api'; import { t } from '$lib/translations'; import { changeQueryParams, dateOptions, errorNotification } from '$lib/common'; + import Tooltip from '$lib/components/Tooltip.svelte'; let buildId: any; let skip = 0; let noMoreBuilds = buildCount < 5 || buildCount <= skip; + + let buildTook = 0; const { id } = $page.params; let preselectedBuildId = $page.url.searchParams.get('buildId'); if (preselectedBuildId) buildId = preselectedBuildId; async function updateBuildStatus({ detail }: { detail: any }) { - const { status } = detail; + const { status, took } = detail; if (status !== 'running') { try { const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`); @@ -58,6 +61,7 @@ if (build.id === buildId) build.status = status; return build; }); + buildTook = took; } } async function loadMoreBuilds() { @@ -137,20 +141,18 @@
{#each builds as build, index (build.id)}
loadBuild(build.id)} class:rounded-tr={index === 0} class:rounded-br={index === builds.length - 1} - class="tooltip tooltip-primary tooltip-top flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" + class="flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" class:bg-coolgray-400={buildId === build.id} class:border-red-500={build.status === 'failed'} class:border-orange-500={build.status === 'canceled'} class:border-green-500={build.status === 'success'} class:border-yellow-500={build.status === 'running'} > -
+
{build.branch || application.branch}
@@ -162,6 +164,10 @@
{#if build.status === 'running'}
{$t('application.build.running')}
+
+ Elapsed + {buildTook}s +
{:else if build.status === 'queued'}
{$t('application.build.queued')}
{:else} @@ -172,6 +178,10 @@ {/if}
+ {new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) + + `\n${build.status}`} {/each}
{#if !noMoreBuilds} diff --git a/apps/ui/src/routes/applications/[id]/logs/index.svelte b/apps/ui/src/routes/applications/[id]/logs/index.svelte index 75a650bfa..160787f1d 100644 --- a/apps/ui/src/routes/applications/[id]/logs/index.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/index.svelte @@ -5,6 +5,7 @@ import { errorNotification } from '$lib/common'; import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import { onMount, onDestroy } from 'svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; let application: any = {}; let logsLoading = false; @@ -38,7 +39,6 @@ logs = data.logs; } } catch (error) { - console.log(error); return errorNotification(error); } finally { logsLoading = false; @@ -146,9 +146,9 @@ {/if}
+ Follow Logs
- Useful for creating staging environments." @@ -194,8 +194,9 @@
- redeploy(container)}>{$t('application.preview.redeploy')}
diff --git a/apps/ui/src/routes/applications/[id]/storages.svelte b/apps/ui/src/routes/applications/[id]/storages.svelte index 0003f6106..c8847582d 100644 --- a/apps/ui/src/routes/applications/[id]/storages.svelte +++ b/apps/ui/src/routes/applications/[id]/storages.svelte @@ -24,7 +24,7 @@ import { page } from '$app/stores'; import Storage from './_Storage.svelte'; import { get } from '$lib/api'; - import Explainer from '$lib/components/Explainer.svelte'; + import SimpleExplainer from '$lib/components/SimpleExplainer.svelte'; import { t } from '$lib/translations'; const { id } = $page.params; @@ -87,7 +87,9 @@
- +
+ +
@@ -107,7 +109,4 @@
-
- -
diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte index 73a971556..5cfa6025e 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -209,13 +209,13 @@
{$t('database.connection_string')} + {#if !isPublic && database.destinationDocker.remoteEngine} + + {/if} - {#if !isPublic && database.destinationDocker.remoteEngine} - - {/if}
changeSettings('isPublic')} @@ -247,6 +248,7 @@ {#if database.type === 'redis'}
changeSettings('appendOnly')} diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte index 3b138bb32..516e887e6 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_MariaDB.svelte @@ -2,8 +2,8 @@ export let database: any; import { status } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import { t } from '$lib/translations'; + import Explainer from '$lib/components/Explainer.svelte';
@@ -37,7 +37,8 @@
{$t('forms.password')} + -
@@ -63,7 +63,7 @@
{$t('forms.roots_password')} -
diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte index 1d5626cca..b93c10bf5 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_MongoDB.svelte @@ -2,8 +2,8 @@ export let database: any; import { status } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import { t } from '$lib/translations'; + import Explainer from '$lib/components/Explainer.svelte';
@@ -23,7 +23,8 @@
{$t('forms.roots_password')} + -
diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte index 13f42a6c9..3405bd125 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_MySQL.svelte @@ -2,8 +2,8 @@ export let database: any; import { status } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import { t } from '$lib/translations'; + import Explainer from '$lib/components/Explainer.svelte';
@@ -37,7 +37,8 @@
{$t('forms.password')} + -
@@ -63,7 +63,8 @@
{$t('forms.roots_password')} + -
diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_PostgreSQL.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_PostgreSQL.svelte index 788d17d56..715ca3e1a 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_PostgreSQL.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_PostgreSQL.svelte @@ -2,8 +2,8 @@ export let database: any; import { status } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import { t } from '$lib/translations'; + import Explainer from '$lib/components/Explainer.svelte';
@@ -26,7 +26,9 @@
Postgres User Password -
@@ -52,7 +53,8 @@
{$t('forms.password')} + -
diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte index d0deeb158..5e47684d0 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_Redis.svelte @@ -2,8 +2,8 @@ export let database: any; import { status } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import { t } from '$lib/translations'; + import Explainer from '$lib/components/Explainer.svelte';
@@ -12,7 +12,8 @@
{$t('forms.password')} + -
diff --git a/apps/ui/src/routes/databases/[id]/__layout.svelte b/apps/ui/src/routes/databases/[id]/__layout.svelte index fd4a81817..bf33f39d1 100644 --- a/apps/ui/src/routes/databases/[id]/__layout.svelte +++ b/apps/ui/src/routes/databases/[id]/__layout.svelte @@ -60,48 +60,53 @@ import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { appSession, status, disabledButton } from '$lib/store'; import DeleteIcon from '$lib/components/DeleteIcon.svelte'; - import Loading from '$lib/components/Loading.svelte'; import { onDestroy, onMount } from 'svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; const { id } = $page.params; - let loading = false; let statusInterval: any = false; + let forceDelete = false; $disabledButton = !$appSession.isAdmin; - async function deleteDatabase() { + async function deleteDatabase(force: boolean) { const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); if (sure) { - loading = true; + $status.database.initialLoading = true; try { - await del(`/databases/${database.id}`, { id: database.id }); + await del(`/databases/${database.id}`, { id: database.id, force }); return await goto('/databases'); } catch (error) { return errorNotification(error); } finally { - loading = false; + $status.database.initialLoading = false; } } } async function stopDatabase() { const sure = confirm($t('database.confirm_stop', { name: database.name })); if (sure) { - loading = true; + $status.database.initialLoading = true; try { await post(`/databases/${database.id}/stop`, {}); - return window.location.reload(); } catch (error) { return errorNotification(error); + } finally { + $status.database.initialLoading = false; } } } async function startDatabase() { - loading = true; + $status.database.initialLoading = true; + $status.database.loading = true; try { await post(`/databases/${database.id}/start`, {}); - return window.location.reload(); } catch (error) { return errorNotification(error); + } finally { + $status.database.initialLoading = false; + $status.database.loading = false; + await getStatus(); } } async function getStatus() { @@ -114,6 +119,9 @@ } onDestroy(() => { $status.database.initialLoading = true; + $status.database.isRunning = false; + $status.database.isExited = false; + $status.database.loading = false; clearInterval(statusInterval); }); onMount(async () => { @@ -137,120 +145,37 @@ {#if id !== 'new'} {/if} diff --git a/apps/ui/src/routes/databases/[id]/logs/index.svelte b/apps/ui/src/routes/databases/[id]/logs/index.svelte index 308d9819c..ee5df54ed 100644 --- a/apps/ui/src/routes/databases/[id]/logs/index.svelte +++ b/apps/ui/src/routes/databases/[id]/logs/index.svelte @@ -6,6 +6,7 @@ import { get } from '$lib/api'; import { t } from '$lib/translations'; import { errorNotification } from '$lib/common'; + import Tooltip from '$lib/components/Tooltip.svelte'; const { id } = $page.params; @@ -17,9 +18,13 @@ let logsEl: any; let position = 0; let loadingLogs = false; - let database: any = {}; + let database = { + name: null + }; onMount(async () => { + const response = await get(`/databases/${id}`); + database = response.database; const { logs: firstLogs } = await get(`/databases/${id}/logs`); logs = firstLogs; loadAllLogs(); @@ -40,7 +45,6 @@ logs = data.logs; } } catch (error) { - console.log(error); return errorNotification(error); } finally { loadingLogs = false; @@ -94,29 +98,6 @@
{database.name}
- - {#if database.fqdn} - - - - - - - {/if}
{#if logs.length === 0} @@ -129,9 +110,9 @@ {/if}
+ Follow Logs
-
-
{$t('forms.configuration')}
+
+
{$t('forms.configuration')}
{#if $appSession.isAdmin}
-
+
-
+
-
+
{#if $appSession.teamId === '0'} -
+

${ + description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${ cannotDisable ? 'You cannot disable this proxy as FQDN is configured for Coolify.' : '' diff --git a/apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte b/apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte index 63e6d3fe9..fc6ee4fec 100644 --- a/apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte +++ b/apps/ui/src/routes/destinations/[id]/_NewLocalDocker.svelte @@ -33,11 +33,7 @@
{$t('forms.configuration')}
-
{#if $appSession.teamId === '0'} -
+
(payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} title={$t('destination.use_coolify_proxy')} - description={$t('destination.new.install_proxy')} + description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'} />
{/if} diff --git a/apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte b/apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte index f754073f9..89ad07f3a 100644 --- a/apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte +++ b/apps/ui/src/routes/destinations/[id]/_NewRemoteDocker.svelte @@ -5,7 +5,7 @@ import { post } from '$lib/api'; import { errorNotification } from '$lib/common'; - import Explainer from '$lib/components/Explainer.svelte'; + import SimpleExplainer from '$lib/components/SimpleExplainer.svelte'; import Setting from '$lib/components/Setting.svelte'; import { t } from '$lib/translations'; @@ -29,22 +29,18 @@
- See docs for more details." />
{$t('forms.configuration')}
-
-
+
(payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} title={$t('destination.use_coolify_proxy')} - description={$t('destination.new.install_proxy')} + description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'} />
diff --git a/apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte b/apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte index dde84860e..c72433ad7 100644 --- a/apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte +++ b/apps/ui/src/routes/destinations/[id]/_RemoteDocker.svelte @@ -38,8 +38,8 @@ } } onMount(async () => { - loading.proxy = true; if (destination.remoteEngine && destination.remoteVerified) { + loading.proxy = true; const { isRunning } = await get(`/destinations/${id}/status`); if (isRunning === false && destination.isCoolifyProxyUsed === true) { destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; @@ -69,6 +69,7 @@ loading.proxy = false; }); async function changeProxySetting() { + if (!destination.remoteVerified) return loading.proxy = true; if (!cannotDisable) { const isProxyActivated = destination.isCoolifyProxyUsed; @@ -259,14 +260,15 @@ />
-
+

${ + description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${ cannotDisable ? 'You cannot disable this proxy as FQDN is configured for Coolify.' : '' diff --git a/apps/ui/src/routes/destinations/[id]/__layout.svelte b/apps/ui/src/routes/destinations/[id]/__layout.svelte index bf774266f..4be92c02f 100644 --- a/apps/ui/src/routes/destinations/[id]/__layout.svelte +++ b/apps/ui/src/routes/destinations/[id]/__layout.svelte @@ -40,7 +40,6 @@ } }; } catch (error) { - console.log(error) return handlerNotFoundLoad(error, url); } }; @@ -56,12 +55,16 @@ import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { appSession } from '$lib/store'; import DeleteIcon from '$lib/components/DeleteIcon.svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; + + const isDestinationDeletable = + (destination?.application.length === 0 && + destination?.database.length === 0 && + destination?.service.length === 0) || + true; - const { id } = $page.params; - const isDestinationDeletable = destination?.application.length === 0 && destination?.database.length === 0 && destination?.service.length === 0 - async function deleteDestination(destination: any) { - if (!isDestinationDeletable) return + if (!isDestinationDeletable) return; const sure = confirm($t('application.confirm_to_delete', { name: destination.name })); if (sure) { try { @@ -74,27 +77,28 @@ } function deletable() { if (!isDestinationDeletable) { - return "Please delete all resources before deleting this." + return 'Please delete all resources before deleting this.'; } if ($appSession.isAdmin) { - return $t('destination.delete_destination') + return $t('destination.delete_destination'); } else { - return $t('destination.permission_denied_delete_destination') + return $t('destination.permission_denied_delete_destination'); } } -{#if id !== 'new'} +{#if $page.params.id !== 'new'} + {deletable()} {/if} diff --git a/apps/ui/src/routes/iam/index.svelte b/apps/ui/src/routes/iam/index.svelte index e50c2fbdd..9b61ec9cc 100644 --- a/apps/ui/src/routes/iam/index.svelte +++ b/apps/ui/src/routes/iam/index.svelte @@ -108,24 +108,21 @@
Identity and Access Management
- +
{#if invitations.length > 0} @@ -170,14 +167,11 @@ {#each accounts as account} - + {account.email}
resetPassword(account.id)}> - +
deleteUser(account.id)}> + Delete {/if} {/if} diff --git a/apps/ui/src/routes/iam/team/[id]/index.svelte b/apps/ui/src/routes/iam/team/[id]/index.svelte index 723a62a53..37fe1b798 100644 --- a/apps/ui/src/routes/iam/team/[id]/index.svelte +++ b/apps/ui/src/routes/iam/team/[id]/index.svelte @@ -12,7 +12,7 @@ export let team: any; export let invitations: any[]; import { page } from '$app/stores'; - import Explainer from '$lib/components/Explainer.svelte'; + import SimpleExplainer from '$lib/components/SimpleExplainer.svelte'; import { post } from '$lib/api'; import { t } from '$lib/translations'; import { errorNotification } from '$lib/common'; @@ -98,7 +98,7 @@
{#if team.id === '0'} - + {/if}
@@ -179,7 +179,7 @@ >
- +
{$t('login.login')} -
-
- - {#if $appSession.whiteLabeledDetails.icon} - Icon for white labeled version of Coolify - {:else} -
Coolify
- {/if} - - - -
- - - -
- +
+ - {#if browser && window.location.host === 'demo.coolify.io'} -
- Registration is open, just fill in an email (does not need - to be live email address for the demo instance) and a password. +
+
+ {#if $appSession.whiteLabeledDetails.icon} +
+ Icon for white labeled version of Coolify +
+ {:else} +
+
+ Coolify icon +
+
+
+

Coolify dashboard

+
+ {/if}
-
- All users gets an own namespace, so you won't be able to - access other users data. +
+
+

Welcome back

+
Please login to continue.
+
+
+ + + +
+ + + +
+
+ {#if browser && window.location.host === 'demo.coolify.io'} +
+ Registration is open, just fill in an email (does not + need to be live email address for the demo instance) and a password. +
+
+ All users gets an own namespace, so you won't be able + to access other users data. +
+ {/if}
- {/if} +
diff --git a/apps/ui/src/routes/register.svelte b/apps/ui/src/routes/register.svelte index 1a2eb7487..0cdf81c4a 100644 --- a/apps/ui/src/routes/register.svelte +++ b/apps/ui/src/routes/register.svelte @@ -61,38 +61,58 @@ } -
goto('/')}> - - - - - - -
-
- {#if $appSession.userId} -
{$t('login.already_logged_in')}
- {:else} -
-
+
+ +
+
+
goto('/')}> + + + + + + +
+
{#if $appSession.whiteLabeledDetails.icon} - Icon for white labeled version of Coolify +
+ Icon for white labeled version of Coolify +
{:else} -
Coolify
+
+
+ Coolify icon +
+
+
+

Coolify dashboard

+
{/if} +
+
+
+
+

Get started

+
Enter the required fields to complete the registration.
+
+ -
+
+ {#if userCount === 0} +
+ {$t('register.first_user')} +
+ {/if}
- {#if userCount === 0} -
- {$t('register.first_user')} -
- {/if} - {/if} +
diff --git a/apps/ui/src/routes/services/[id]/_Secret.svelte b/apps/ui/src/routes/services/[id]/_Secret.svelte index c347dcabc..fa1f95318 100644 --- a/apps/ui/src/routes/services/[id]/_Secret.svelte +++ b/apps/ui/src/routes/services/[id]/_Secret.svelte @@ -1,4 +1,4 @@ -
-
Ghost
- +
+ Ghost +
diff --git a/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte b/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte index 03fcc3ef4..c4b4dc5e6 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_GlitchTip.svelte @@ -1,17 +1,51 @@ @@ -19,109 +53,121 @@
GlitchTip
-
-
Settings
-
-
changeSettings('enableOpenUserRegistration')} + title="Enable Open User Registration" + description={''} + /> +
Email settings
+
+ changeSettings('emailSmtpUseTls')} + title="Use TLS for SMTP" + description={''} + /> +
- + changeSettings('emailSmtpUseSsl')} + title="Use SSL for SMTP" + description={''} + /> +
+
+
- +
- +
- +
- +
- Email Backend +
- -
- -
- - -
- -
- +
- +
@@ -130,35 +176,31 @@
- +
- +
- +
- +
- +
- + diff --git a/apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte b/apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte index b097ec84f..cb26d5767 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Hasura.svelte @@ -1,6 +1,5 @@ diff --git a/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte b/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte index 8532896dc..e1992f56d 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte @@ -11,7 +11,11 @@
Plausible Analytics
- + -
diff --git a/apps/ui/src/routes/services/[id]/_Services/_Services.svelte b/apps/ui/src/routes/services/[id]/_Services/_Services.svelte index b7d365680..0c05271b6 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Services.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Services.svelte @@ -14,7 +14,6 @@ import { t } from '$lib/translations'; import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; - import Explainer from '$lib/components/Explainer.svelte'; import Setting from '$lib/components/Setting.svelte'; import Fider from './_Fider.svelte'; @@ -30,6 +29,9 @@ import Appwrite from './_Appwrite.svelte'; import Moodle from './_Moodle.svelte'; import Searxng from './_Searxng.svelte'; + import Weblate from './_Weblate.svelte'; + import Explainer from '$lib/components/Explainer.svelte'; + import Taiga from './_Taiga.svelte'; const { id } = $page.params; $: isDisabled = @@ -282,8 +284,9 @@
- - +
- + >{$t('application.url_fqdn')} + +
- + -
Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} - />
{#if service.type === 'plausibleanalytics'} @@ -405,6 +410,10 @@ {:else if service.type === 'searxng'} + {:else if service.type === 'weblate'} + + {:else if service.type === 'taiga'} + {/if}
diff --git a/apps/ui/src/routes/services/[id]/_Services/_Taiga.svelte b/apps/ui/src/routes/services/[id]/_Services/_Taiga.svelte new file mode 100644 index 000000000..1a29943e9 --- /dev/null +++ b/apps/ui/src/routes/services/[id]/_Services/_Taiga.svelte @@ -0,0 +1,118 @@ + + +
+
Taiga
+
+ +
+ + +
+ +
+
Django
+
+ +
+ + +
+
+ + +
+
+
RabbitMQ
+
+ +
+ + +
+
+ + +
+ +
+
PostgreSQL
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/apps/ui/src/routes/services/[id]/_Services/_Umami.svelte b/apps/ui/src/routes/services/[id]/_Services/_Umami.svelte index 831444b2d..9e6d065a1 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Umami.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Umami.svelte @@ -1,7 +1,6 @@ @@ -13,7 +12,11 @@
- + -
diff --git a/apps/ui/src/routes/services/[id]/_Services/_Weblate.svelte b/apps/ui/src/routes/services/[id]/_Services/_Weblate.svelte new file mode 100644 index 000000000..fdbd055c1 --- /dev/null +++ b/apps/ui/src/routes/services/[id]/_Services/_Weblate.svelte @@ -0,0 +1,66 @@ + + +
+
Weblate
+
+ +
+ + +
+ +
+
PostgreSQL
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte b/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte index 4d72fafd2..474aa2f13 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte @@ -93,6 +93,7 @@ define('SUBDOMAIN_INSTALL', false);`
import { page } from '$app/stores'; import DeleteIcon from '$lib/components/DeleteIcon.svelte'; - import Loading from '$lib/components/Loading.svelte'; import { del, get, post } from '$lib/api'; import { goto } from '$app/navigation'; import { t } from '$lib/translations'; import { errorNotification, handlerNotFoundLoad } from '$lib/common'; - import { appSession, disabledButton, status, location, setLocation } from '$lib/store'; + import { + appSession, + isDeploymentEnabled, + status, + location, + setLocation, + checkIfDeploymentEnabledServices + } from '$lib/store'; import { onDestroy, onMount } from 'svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; const { id } = $page.params; export let service: any; - $disabledButton = - !$appSession.isAdmin || - !service.fqdn || - !service.destinationDocker || - !service.version || - !service.type; + $isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service); - let loading = false; let statusInterval: any; async function deleteService() { const sure = confirm($t('application.confirm_to_delete', { name: service.name })); if (sure) { - loading = true; + $status.service.initialLoading = true; try { if (service.type && $status.service.isRunning) await post(`/services/${service.id}/${service.type}/stop`, {}); @@ -89,31 +89,34 @@ } catch (error) { return errorNotification(error); } finally { - loading = false; + $status.service.initialLoading = false; } } } async function stopService() { const sure = confirm($t('database.confirm_stop', { name: service.name })); if (sure) { - loading = true; + $status.service.initialLoading = true; try { await post(`/services/${service.id}/${service.type}/stop`, {}); } catch (error) { return errorNotification(error); } finally { - loading = false; + $status.service.initialLoading = false; } } } async function startService() { - loading = true; + $status.service.initialLoading = true; + $status.service.loading = true; try { await post(`/services/${service.id}/${service.type}/start`, {}); } catch (error) { return errorNotification(error); } finally { - loading = false; + $status.service.initialLoading = false; + $status.service.loading = false; + await getStatus(); } } async function getStatus() { @@ -127,7 +130,11 @@ } onDestroy(() => { $status.service.initialLoading = true; + $status.service.isRunning = false; + $status.service.isExited = false; + $status.service.loading = false; $location = null; + $isDeploymentEnabled = false; clearInterval(statusInterval); }); onMount(async () => { @@ -146,269 +153,266 @@ diff --git a/apps/ui/src/routes/services/[id]/logs/index.svelte b/apps/ui/src/routes/services/[id]/logs/index.svelte index f2c4d3229..bb9c775c3 100644 --- a/apps/ui/src/routes/services/[id]/logs/index.svelte +++ b/apps/ui/src/routes/services/[id]/logs/index.svelte @@ -5,6 +5,7 @@ import { t } from '$lib/translations'; import { errorNotification } from '$lib/common'; import { onDestroy, onMount } from 'svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; let service: any = {}; let logsLoading = false; @@ -40,7 +41,6 @@ logs = data.logs; } } catch (error) { - console.log(error); return errorNotification(error); } finally { logsLoading = false; @@ -126,9 +126,9 @@ {/if}
+ Follow Logs
!secret.startsWith('#') && secret) + .map((secret) => { + const [name, ...rest] = secret.split('='); + const value = rest.join('='); + const cleanValue = value?.replaceAll('"', '') || ''; + return { + name, + value: cleanValue, + isNew: !secrets.find((secret: any) => name === secret.name) + }; + }); + + await Promise.all( + batchSecretsPairs.map(({ name, value, isNew }) => + limit(() => saveSecret({ name, value, serviceId: id, isNew })) + ) + ); + batchSecrets = ''; + await refreshSecrets(); + addToast({ + message: 'Secrets saved.', + type: 'success' + }); + }
+

Paste .env file

+
+