diff --git a/.gitignore b/.gitignore index 500cd6776..eddb26abd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ client apps/api/db/*.db local-serve apps/api/db/migration.db-journal -apps/api/core* \ No newline at end of file +apps/api/core* +logs \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 958b6512f..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,256 +0,0 @@ -# 👋 Welcome - -First of all, thank you for considering contributing to my project! It means a lot 💜. - - -## 🙋 Want to help? - -If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide. - -Follow the [introduction](#introduction) to get started then start contributing! - -This is a little list of what you can do to help the project: - -- [🧑‍💻 Develop your own ideas](#developer-contribution) -- [🌐 Translate the project](#translation) - -## 👋 Introduction - -### Setup with 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. - -### Setup with 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. - -### Setup locally in your machine - -> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces. - -#### Recommended Pull Request Guideline - -- Fork the project -- Clone your fork repo to local -- Create a new branch -- Push to your fork repo -- Create a pull request: https://github.com/coollabsio/coolify/compare -- Write a proper description -- Open the pull request to review against `next` branch - ---- - -# 🧑‍💻 Developer contribution -## 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** - ---- - -## How to start after you set up your local fork? - -### Prerequisites -1. 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! - -2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. -3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally. -4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally. - -Optional: - -4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally. - -### Steps for local setup - -1. Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool. -2. Install dependencies with `pnpm install`. -3. Need to create a local SQlite database with `pnpm db:push`. - - This will apply all migrations at `db/dev.db`. - -4. Seed the database with base entities with `pnpm db:seed` -5. You can start coding after starting `pnpm dev`. - ---- - -## Database migrations - -During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process. - -If the schema is finalized, you need to create a migration file with `pnpm db:migrate ` where `nameOfMigration` is given by you. Make it sense. :) - ---- - -## How to add new services - -You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true: - -- Self-hostable (obviously) -- Open-source -- Maintained (I do not want to add software full of bugs) - -## Backend - -There are 5 steps you should make on the backend side. - -1. Create Prisma / database schema for the new service. -2. Add supported versions of the service. -3. Update global functions. -4. Create API endpoints. -5. Define automatically generated variables. - -> I will use [Umami](https://umami.is/) as an example service. - -### Create Prisma / Database schema for the new service. - -You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB. - -Update Prisma schema in [prisma/schema.prisma](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. - -If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command. - -> You must restart the running development environment to be able to use the new model - -> If you use VSCode/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment. - -### Add supported versions - -Supported versions are hardcoded into Coolify (for now). - -You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON: - -```js - { - // Name used to identify the service internally - name: 'umami', - // Fancier name to show to the user - fancyName: 'Umami', - // Docker base image for the service - baseImage: 'ghcr.io/mikecao/umami', - // Optional: If there is any dependent image, you should list it here - images: [], - // Usable tags - versions: ['postgresql-latest'], - // Which tag is the recommended - recommendedVersion: 'postgresql-latest', - // Application's default port, Umami listens on 3000 - ports: { - main: 3000 - } - } -``` - -### Add required functions/properties - -1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required. - -```js -const include: any = { - destinationDocker: true, - persistentStorage: true, - serviceSecret: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true, - umami: true // This line! -}; -``` - -2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable). - -```js -[...] -else if (type === 'umami') { - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword()); - const postgresqlDatabase = 'umami'; - const hashSalt = encrypt(generatePassword(64)); - await prisma.service.update({ - where: { id }, - data: { - type, - umami: { - create: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - hashSalt, - } - } - } - }); - } -``` - -3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly) - -```js -export const umami = [{ - name: 'postgresqlUser', - isEditable: false, - isLowerCase: false, - isNumber: false, - isBoolean: false, - isEncrypted: false -}] -``` - -4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.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) - -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. - -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 - -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. - - > For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte). - - -Good job! 👏 - - diff --git a/CONTRIBUTION_NEW.md b/CONTRIBUTION.md similarity index 51% rename from CONTRIBUTION_NEW.md rename to CONTRIBUTION.md index a9ed208ed..78992243b 100644 --- a/CONTRIBUTION_NEW.md +++ b/CONTRIBUTION.md @@ -1,45 +1,3 @@ ---- -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! :) @@ -100,9 +58,58 @@ All data that needs to be persist for a service should be saved to the database 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). +Update Prisma schema in [src/apps/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. + +Once done, create Prisma schema with `pnpm db:push`. +> You may also need to restart `Typescript Language Server` in your IDE to get the new types. + +### Add available versions + +Versions are hardcoded into Coolify at the moment and based on Docker image tags. +- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts) + +### Include the new service in queries + +At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required. + +### Define auto-generated fields + +At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable). + +### Define input field details + +At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc). + +### Define the start process + +- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions. + +> See `startUmamiService()` function as example. + +- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function. + +### Define the deletion process + +[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process. + +### Custom logo + +- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](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. + +- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property. + +- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service. + +### Custom fields on the UI +By default the URL and name are shown on the UI. Everything else needs to be added [here](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 [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte). + +Good job! 👏 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 675fe5b5b..b7bd56212 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) COPY --from=build /app/apps/api/build/ . +COPY --from=build /app/others/fluentbit/ ./fluentbit COPY --from=build /app/apps/ui/build/ ./public COPY --from=build /app/apps/api/prisma/ ./prisma COPY --from=build /app/apps/api/package.json . diff --git a/apps/api/package.json b/apps/api/package.json index f436482c8..f3635517b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -29,6 +29,8 @@ "bree": "9.1.2", "cabin": "9.1.2", "compare-versions": "5.0.1", + "csv-parse": "^5.3.0", + "csvtojson": "^2.0.10", "cuid": "2.1.8", "dayjs": "1.11.5", "dockerode": "3.3.4", diff --git a/apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql b/apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql new file mode 100644 index 000000000..0ec1aafa0 --- /dev/null +++ b/apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql @@ -0,0 +1,18 @@ +-- AlterTable +ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT; + +-- CreateTable +CREATE TABLE "PreviewApplication" ( + "id" TEXT NOT NULL PRIMARY KEY, + "pullmergeRequestId" TEXT NOT NULL, + "sourceBranch" TEXT NOT NULL, + "isRandomDomain" BOOLEAN NOT NULL DEFAULT false, + "customDomain" TEXT, + "applicationId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PreviewApplication_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "PreviewApplication_applicationId_key" ON "PreviewApplication"("applicationId"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index df7516e01..a7500539d 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -119,6 +119,19 @@ model Application { secrets Secret[] teams Team[] connectedDatabase ApplicationConnectedDatabase? + previewApplication PreviewApplication[] +} + +model PreviewApplication { + id String @id @default(cuid()) + pullmergeRequestId String + sourceBranch String + isRandomDomain Boolean @default(false) + customDomain String? + applicationId String @unique + application Application @relation(fields: [applicationId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model ApplicationConnectedDatabase { @@ -210,21 +223,22 @@ model BuildLog { } model Build { - id String @id @default(cuid()) - type String - applicationId String? - destinationDockerId String? - gitSourceId String? - githubAppId String? - gitlabAppId String? - commit String? - pullmergeRequestId String? - forceRebuild Boolean @default(false) - sourceBranch String? - branch String? - status String? @default("queued") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + type String + applicationId String? + destinationDockerId String? + gitSourceId String? + githubAppId String? + gitlabAppId String? + commit String? + pullmergeRequestId String? + previewApplicationId String? + forceRebuild Boolean @default(false) + sourceBranch String? + branch String? + status String? @default("queued") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model DestinationDocker { diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 945883bb2..55a7ef461 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -38,8 +38,16 @@ import * as buildpacks from '../lib/buildPacks'; for (const queueBuild of queuedBuilds) { actions.push(async () => { let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) - let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild + let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild application = decryptApplication(application) + const originalApplicationId = application.id + if (pullmergeRequestId) { + const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } }) + if (previewApplications.length > 0) { + previewApplicationId = previewApplications[0].id + } + } + const usableApplicationId = previewApplicationId || originalApplicationId try { if (queueBuild.status === 'running') { await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); @@ -104,17 +112,17 @@ import * as buildpacks from '../lib/buildPacks'; ) .digest('hex'); const { debug } = settings; - if (concurrency === 1) { - await prisma.build.updateMany({ - where: { - status: { in: ['queued', 'running'] }, - id: { not: buildId }, - applicationId, - createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) } - }, - data: { status: 'failed' } - }); - } + // if (concurrency === 1) { + // await prisma.build.updateMany({ + // where: { + // status: { in: ['queued', 'running'] }, + // id: { not: buildId }, + // applicationId, + // createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) } + // }, + // data: { status: 'failed' } + // }); + // } let imageId = applicationId; let domain = getDomain(fqdn); const volumes = @@ -261,7 +269,10 @@ import * as buildpacks from '../lib/buildPacks'; if (secrets.length > 0) { secrets.forEach((secret) => { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { envs.push(`${secret.name}=${secret.value}`); } } else { @@ -335,10 +346,15 @@ import * as buildpacks from '../lib/buildPacks'; await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); - await prisma.build.updateMany({ - where: { id: buildId, status: { in: ['queued', 'running'] } }, - data: { status: 'failed' } - }); + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + if (foundBuild) { + await prisma.build.update({ + where: { id: buildId }, + data: { + status: 'failed' + } + }); + } throw new Error(error); } await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); @@ -350,11 +366,18 @@ import * as buildpacks from '../lib/buildPacks'; } } catch (error) { - await prisma.build.updateMany({ - where: { id: buildId, status: { in: ['queued', 'running'] } }, - data: { status: 'failed' } - }); - await saveBuildLog({ line: error, buildId, applicationId: application.id }); + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + if (foundBuild) { + await prisma.build.update({ + where: { id: buildId }, + data: { + status: 'failed' + } + }); + } + if (error !== 1) { + await saveBuildLog({ line: error, buildId, applicationId: application.id }); + } } }); } diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index 178a06aca..23e380610 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -29,7 +29,7 @@ async function autoUpdater() { `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .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"` + `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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` ); } } else { diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 93b8ca076..44812a76d 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -1,4 +1,4 @@ -import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; +import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; import { promises as fs } from 'fs'; import { day } from "../dayjs"; @@ -461,17 +461,32 @@ export const saveBuildLog = async ({ buildId: string; applicationId: string; }): Promise => { + const { default: got } = await import('got') + if (line && typeof line === 'string' && line.includes('ghs_')) { const regex = /ghs_.*@/g; line = line.replace(regex, '@'); } const addTimestamp = `[${generateTimestamp()}] ${line}`; - if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`); - return await prisma.buildLog.create({ - data: { - line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId - } - }); + const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224'; + + if (isDev) { + console.debug(`[${applicationId}] ${addTimestamp}`); + } + try { + return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, { + json: { + line: encrypt(line) + } + }) + } catch(error) { + return await prisma.buildLog.create({ + data: { + line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId + } + }); + } + }; export async function copyBaseConfigurationFiles( @@ -556,7 +571,6 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma ); } - export async function buildImage({ applicationId, tag, @@ -677,8 +691,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) { secrets, pullmergeRequestId } = data; - - const isPnpm = checkPnpm(installCommand, buildCommand); const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild}`); @@ -688,7 +700,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { @@ -722,7 +737,10 @@ export async function buildCacheImageForLaravel(data, imageForBuild) { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/deno.ts b/apps/api/src/lib/buildPacks/deno.ts index 6dafa55ed..f255a5983 100644 --- a/apps/api/src/lib/buildPacks/deno.ts +++ b/apps/api/src/lib/buildPacks/deno.ts @@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/docker.ts b/apps/api/src/lib/buildPacks/docker.ts index 04041b190..c017b4bed 100644 --- a/apps/api/src/lib/buildPacks/docker.ts +++ b/apps/api/src/lib/buildPacks/docker.ts @@ -28,6 +28,7 @@ export default async function (data) { if (secrets.length > 0) { secrets.forEach((secret) => { if (secret.isBuildSecret) { + // TODO: fix secrets if ( (pullmergeRequestId && secret.isPRMRSecret) || (!pullmergeRequestId && !secret.isPRMRSecret) diff --git a/apps/api/src/lib/buildPacks/nextjs.ts b/apps/api/src/lib/buildPacks/nextjs.ts index 90d80449e..90a70f89b 100644 --- a/apps/api/src/lib/buildPacks/nextjs.ts +++ b/apps/api/src/lib/buildPacks/nextjs.ts @@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/node.ts b/apps/api/src/lib/buildPacks/node.ts index ad234aed1..546942542 100644 --- a/apps/api/src/lib/buildPacks/node.ts +++ b/apps/api/src/lib/buildPacks/node.ts @@ -23,7 +23,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/nuxtjs.ts b/apps/api/src/lib/buildPacks/nuxtjs.ts index 90d80449e..90a70f89b 100644 --- a/apps/api/src/lib/buildPacks/nuxtjs.ts +++ b/apps/api/src/lib/buildPacks/nuxtjs.ts @@ -27,7 +27,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/php.ts b/apps/api/src/lib/buildPacks/php.ts index e6dc1699f..eaf97d1b1 100644 --- a/apps/api/src/lib/buildPacks/php.ts +++ b/apps/api/src/lib/buildPacks/php.ts @@ -16,7 +16,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/python.ts b/apps/api/src/lib/buildPacks/python.ts index 110471179..36d707f16 100644 --- a/apps/api/src/lib/buildPacks/python.ts +++ b/apps/api/src/lib/buildPacks/python.ts @@ -21,7 +21,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/buildPacks/static.ts b/apps/api/src/lib/buildPacks/static.ts index 47fa0dfa4..c727985ca 100644 --- a/apps/api/src/lib/buildPacks/static.ts +++ b/apps/api/src/lib/buildPacks/static.ts @@ -24,7 +24,10 @@ const createDockerfile = async (data, image): Promise => { secrets.forEach((secret) => { if (secret.isBuildSecret) { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); + } else { Dockerfile.push(`ARG ${secret.name}=${secret.value}`); } } else { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 50a8dd4db..6667f78af 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -1,4 +1,4 @@ -import { exec } from 'node:child_process' +import { exec } from 'node:child_process'; import util from 'util'; import fs from 'fs/promises'; import yaml from 'js-yaml'; @@ -11,17 +11,17 @@ 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 sshConfig from 'ssh-config'; import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; -import * as serviceFields from './services/serviceFields' +import * as serviceFields from './services/serviceFields'; import { saveBuildLog } from './buildPacks/common'; import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.10.0'; +export const version = '3.10.5'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -38,24 +38,24 @@ export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; export const defaultTraefikImage = `traefik:v2.8`; export function getAPIUrl() { if (process.env.GITPOD_WORKSPACE_URL) { - const { href } = new URL(process.env.GITPOD_WORKSPACE_URL) - const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') - return newURL + const { href } = new URL(process.env.GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + return newURL; } if (process.env.CODESANDBOX_HOST) { - return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}` + return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; } - return isDev ? 'http://localhost:3001' : 'http://localhost:3000'; + return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000'; } export function getUIUrl() { if (process.env.GITPOD_WORKSPACE_URL) { - const { href } = new URL(process.env.GITPOD_WORKSPACE_URL) - const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '') - return newURL + const { href } = new URL(process.env.GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, ''); + return newURL; } if (process.env.CODESANDBOX_HOST) { - return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}` + return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`; } return 'http://localhost:3000'; } @@ -68,56 +68,71 @@ const otherTraefikEndpoint = isDev ? `${getAPIUrl()}/webhooks/traefik/other.json` : 'http://coolify:3000/webhooks/traefik/other.json'; - export const uniqueName = (): string => uniqueNamesGenerator(customConfig); export const asyncExecShell = util.promisify(exec); -export const asyncExecShellStream = async ({ debug, buildId, applicationId, command, engine }: { debug: boolean, buildId: string, applicationId: string, command: string, engine: string }) => { +export const asyncExecShellStream = async ({ + debug, + buildId, + applicationId, + command, + engine +}: { + debug: boolean; + buildId: string; + applicationId: string; + command: string; + engine: string; +}) => { return await new Promise(async (resolve, reject) => { - const { execaCommand } = await import('execa') - const subprocess = execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } }) + const { execaCommand } = await import('execa'); + const subprocess = execaCommand(command, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); if (debug) { subprocess.stdout.on('data', async (data) => { const stdout = data.toString(); - const array = stdout.split('\n') + const array = stdout.split('\n'); for (const line of array) { if (line !== '\n' && line !== '') { - await saveBuildLog({ + logs.push(line.replace('\n', '')) + debug && await saveBuildLog({ line: `${line.replace('\n', '')}`, buildId, applicationId }); } } - }) + }); subprocess.stderr.on('data', async (data) => { const stderr = data.toString(); - const array = stderr.split('\n') + const array = stderr.split('\n'); for (const line of array) { if (line !== '\n' && line !== '') { - await saveBuildLog({ + errorLogs.push(line.replace('\n', '')) + debug && await saveBuildLog({ line: `${line.replace('\n', '')}`, buildId, applicationId - }); + }); } } - }) + }); } subprocess.on('exit', async (code) => { await asyncSleep(1000); if (code === 0) { - resolve(code) + resolve(code); } else { - reject(code) + reject(code); } - }) - }) -} + }); + }); +}; export const asyncSleep = (delay: number): Promise => new Promise((resolve) => setTimeout(resolve, delay)); export const prisma = new PrismaClient({ - errorFormat: 'minimal', + errorFormat: 'minimal' // log: [ // { // emit: 'event', @@ -165,10 +180,9 @@ export const decrypt = (hashString: string) => { ]); return decrpyted.toString(); } catch (error) { - console.log({ decryptionError: error.message }) - return hashString + console.log({ decryptionError: error.message }); + return hashString; } - } }; export const encrypt = (text: string) => { @@ -183,8 +197,6 @@ export const encrypt = (text: string) => { } }; - - export async function checkDoubleBranch(branch: string, projectId: number): Promise { const applications = await prisma.application.findMany({ where: { branch, projectId } }); return applications.length > 1; @@ -203,7 +215,7 @@ export async function isDNSValid(hostname: any, domain: string): Promise { resolves = await dns.resolve4(hostname); } } catch (error) { - throw 'Invalid DNS.' + throw 'Invalid DNS.'; } try { @@ -218,11 +230,10 @@ export async function isDNSValid(hostname: any, domain: string): Promise { } if (!ipDomainFound) throw false; } catch (error) { - throw 'DNS not set' + throw 'DNS not set'; } } - export function getDomain(domain: string): string { return domain?.replace('https://', '').replace('http://', ''); } @@ -248,7 +259,7 @@ export async function isDomainConfigured({ ], id: { not: id }, destinationDocker: { - remoteIpAddress, + remoteIpAddress } }, select: { fqdn: true } @@ -284,7 +295,10 @@ export async function isDomainConfigured({ export async function getContainerUsage(dockerId: string, container: string): Promise { try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` }) + const { stdout } = await executeDockerCmd({ + dockerId, + command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` + }); return JSON.parse(stdout); } catch (err) { return { @@ -313,7 +327,7 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P resolves = await dns.resolve4(hostname); } } catch (error) { - throw { status: 500, message: `Could not determine IP address for ${hostname}.` } + throw { status: 500, message: `Could not determine IP address for ${hostname}.` }; } if (dualCerts) { @@ -335,9 +349,15 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P } } if (ipDomainFound && ipDomainDualCertFound) return { status: 200 }; - throw { status: 500, message: `DNS not set correctly or propogated.
Please check your DNS settings.` } + throw { + status: 500, + message: `DNS not set correctly or propogated.
Please check your DNS settings.` + }; } catch (error) { - throw { status: 500, message: `DNS not set correctly or propogated.
Please check your DNS settings.` } + throw { + status: 500, + message: `DNS not set correctly or propogated.
Please check your DNS settings.` + }; } } else { try { @@ -349,9 +369,15 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P } } if (ipDomainFound) return { status: 200 }; - throw { status: 500, message: `DNS not set correctly or propogated.
Please check your DNS settings.` } + throw { + status: 500, + message: `DNS not set correctly or propogated.
Please check your DNS settings.` + }; } catch (error) { - throw { status: 500, message: `DNS not set correctly or propogated.
Please check your DNS settings.` } + throw { + status: 500, + message: `DNS not set correctly or propogated.
Please check your DNS settings.` + }; } } } @@ -425,63 +451,85 @@ export const supportedDatabaseTypesAndVersions = [ export async function getFreeSSHLocalPort(id: string): Promise { const { default: isReachable } = await import('is-port-reachable'); - const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({ where: { id } }) + const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({ + where: { id } + }); if (sshLocalPort) { - return Number(sshLocalPort) + return Number(sshLocalPort); } const data = await prisma.setting.findFirst(); const { minPort, maxPort } = data; - const ports = await prisma.destinationDocker.findMany({ where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } } }) + const ports = await prisma.destinationDocker.findMany({ + where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } } + }); const alreadyConfigured = await prisma.destinationDocker.findFirst({ where: { - remoteIpAddress, id: { not: id }, sshLocalPort: { not: null } + remoteIpAddress, + id: { not: id }, + sshLocalPort: { not: null } } - }) + }); if (alreadyConfigured?.sshLocalPort) { - await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: alreadyConfigured.sshLocalPort } }) - return Number(alreadyConfigured.sshLocalPort) + await prisma.destinationDocker.update({ + where: { id }, + data: { sshLocalPort: alreadyConfigured.sshLocalPort } + }); + return Number(alreadyConfigured.sshLocalPort); } - const range = generateRangeArray(minPort, maxPort) - const availablePorts = range.filter(port => !ports.map(p => p.sshLocalPort).includes(port)) + const range = generateRangeArray(minPort, maxPort); + const availablePorts = range.filter((port) => !ports.map((p) => p.sshLocalPort).includes(port)); for (const port of availablePorts) { - const found = await isReachable(port, { host: 'localhost' }) + const found = await isReachable(port, { host: 'localhost' }); if (!found) { - await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: Number(port) } }) - return Number(port) + await prisma.destinationDocker.update({ + where: { id }, + data: { sshLocalPort: Number(port) } + }); + return Number(port); } } - return false + return false; } export async function createRemoteEngineConfiguration(id: string) { const homedir = os.homedir(); - const sshKeyFile = `/tmp/id_rsa-${id}` + const sshKeyFile = `/tmp/id_rsa-${id}`; const localPort = await getFreeSSHLocalPort(id); - const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }) - await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }) + 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 coolify-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) { 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 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/coolify-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`) + 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/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`) - } catch (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('') - const found = config.find({ Host: remoteIpAddress }) + const config = sshConfig.parse(''); + const found = config.find({ Host: remoteIpAddress }); if (!found) { config.append({ Host: remoteIpAddress, @@ -490,14 +538,14 @@ export async function createRemoteEngineConfiguration(id: string) { User: remoteUser, IdentityFile: sshKeyFile, StrictHostKeyChecking: 'no' - }) + }); } try { - await fs.stat(`${homedir}/.ssh/`) + await fs.stat(`${homedir}/.ssh/`); } catch (error) { - await fs.mkdir(`${homedir}/.ssh/`) + await fs.mkdir(`${homedir}/.ssh/`); } - return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)) + return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)); } export async function executeSSHCmd({ dockerId, command }) { const { execaCommand } = await import('execa') @@ -520,14 +568,14 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId const { execaCommand } = await import('execa') let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { - await createRemoteEngineConfiguration(dockerId) - engine = `ssh://${remoteIpAddress}` + await createRemoteEngineConfiguration(dockerId); + engine = `ssh://${remoteIpAddress}`; } else { - engine = 'unix:///var/run/docker.sock' + engine = 'unix:///var/run/docker.sock'; } if (process.env.CODESANDBOX_HOST) { if (command.startsWith('docker compose')) { - command = command.replace(/docker compose/gi, 'docker-compose') + command = command.replace(/docker compose/gi, 'docker-compose'); } } if (command.startsWith(`docker build --progress plain`)) { @@ -537,26 +585,35 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId } export async function startTraefikProxy(id: string): Promise { const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) - const found = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true }); + const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true }); const { id: settingsId, ipv4, ipv6 } = await listSettings(); if (!found) { - const { stdout: coolifyNetwork } = await executeDockerCmd({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` }) + const { stdout: coolifyNetwork } = await executeDockerCmd({ + dockerId: id, + command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` + }); if (!coolifyNetwork) { - await executeDockerCmd({ dockerId: id, command: `docker network create --attachable coolify-infra` }) + await executeDockerCmd({ + dockerId: id, + command: `docker network create --attachable coolify-infra` + }); } - const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }) + const { stdout: Config } = await executeDockerCmd({ + dockerId: id, + command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` + }); const ip = JSON.parse(Config)[0].Gateway; - let traefikUrl = mainTraefikEndpoint + let traefikUrl = mainTraefikEndpoint; if (remoteEngine) { - let ip = null + let ip = null; if (isDev) { - ip = getAPIUrl() + ip = getAPIUrl(); } else { - ip = `http://${ipv4 || ipv6}:3000` + ip = `http://${ipv4 || ipv6}:3000`; } - traefikUrl = `${ip}/webhooks/traefik/remote/${id}` + traefikUrl = `${ip}/webhooks/traefik/remote/${id}`; } await executeDockerCmd({ dockerId: id, @@ -582,7 +639,7 @@ export async function startTraefikProxy(id: string): Promise { --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ --log.level=error` - }) + }); await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } }); await prisma.destinationDocker.update({ where: { id }, @@ -606,22 +663,24 @@ export async function startTraefikProxy(id: string): Promise { } export async function configureNetworkTraefikProxy(destination: any): Promise { - const { id } = destination + const { id } = destination; const { stdout: networks } = await executeDockerCmd({ dockerId: id, - command: - `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` }); const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); if (!configuredNetworks.includes(destination.network)) { - await executeDockerCmd({ dockerId: destination.id, command: `docker network connect ${destination.network} coolify-proxy` }) + await executeDockerCmd({ + dockerId: destination.id, + command: `docker network connect ${destination.network} coolify-proxy` + }); } } export async function stopTraefikProxy( id: string ): Promise<{ stdout: string; stderr: string } | Error> { - const found = await checkContainer({ dockerId: id, container: 'coolify-proxy' }); + const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' }); await prisma.destinationDocker.update({ where: { id }, data: { isCoolifyProxyUsed: false } @@ -632,10 +691,8 @@ export async function stopTraefikProxy( if (found) { await executeDockerCmd({ dockerId: id, - command: - `docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy` }); - } } catch (error) { return error; @@ -648,10 +705,13 @@ export async function listSettings(): Promise { return settings; } - -export function generatePassword({ length = 24, symbols = false, isHex = false }: { length?: number, symbols?: boolean, isHex?: boolean } | null): string { +export function generatePassword({ + length = 24, + symbols = false, + isHex = false +}: { length?: number; symbols?: boolean; isHex?: boolean } | null): string { if (isHex) { - return crypto.randomBytes(length).toString("hex"); + return crypto.randomBytes(length).toString('hex'); } const password = generator.generate({ length, @@ -678,32 +738,32 @@ type DatabaseConfiguration = { }; } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MONGO_INITDB_ROOT_USERNAME?: string; - MONGO_INITDB_ROOT_PASSWORD?: string; - MONGODB_ROOT_USER?: string; - MONGODB_ROOT_PASSWORD?: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MONGO_INITDB_ROOT_USERNAME?: string; + MONGO_INITDB_ROOT_PASSWORD?: string; + MONGODB_ROOT_USER?: string; + MONGODB_ROOT_PASSWORD?: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MARIADB_ROOT_USER: string; - MARIADB_ROOT_PASSWORD: string; - MARIADB_USER: string; - MARIADB_PASSWORD: string; - MARIADB_DATABASE: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MARIADB_ROOT_USER: string; + MARIADB_ROOT_PASSWORD: string; + MARIADB_USER: string; + MARIADB_PASSWORD: string; + MARIADB_DATABASE: string; + }; + } | { volume: string; image: string; @@ -780,11 +840,11 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data image: `${baseImage}:${version}`, volume: `${id}-${type}-data:/bitnami/mysql/data`, ulimits: {} - } + }; if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/var/lib/mysql`; } - return configuration + return configuration; } else if (type === 'mariadb') { const configuration: DatabaseConfiguration = { privatePort: 3306, @@ -802,7 +862,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/var/lib/mysql`; } - return configuration + return configuration; } else if (type === 'mongodb') { const configuration: DatabaseConfiguration = { privatePort: 27017, @@ -818,10 +878,10 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data configuration.environmentVariables = { MONGO_INITDB_ROOT_USERNAME: rootUser, MONGO_INITDB_ROOT_PASSWORD: rootUserPassword - } + }; configuration.volume = `${id}-${type}-data:/data/db`; } - return configuration + return configuration; } else if (type === 'postgresql') { const configuration: DatabaseConfiguration = { privatePort: 5432, @@ -834,16 +894,16 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data image: `${baseImage}:${version}`, volume: `${id}-${type}-data:/bitnami/postgresql`, ulimits: {} - } + }; if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/var/lib/postgresql`; configuration.environmentVariables = { POSTGRES_PASSWORD: dbUserPassword, POSTGRES_USER: dbUser, POSTGRES_DB: defaultDatabase - } + }; } - return configuration + return configuration; } else if (type === 'redis') { const { settings: { appendOnly } } = database; const configuration: DatabaseConfiguration = { @@ -859,9 +919,11 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data }; if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/data`; - configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no'} --requirepass ${dbUserPassword}`; + configuration.command = `/usr/local/bin/redis-server --appendonly ${ + appendOnly ? 'yes' : 'no' + } --requirepass ${dbUserPassword}`; } - return configuration + return configuration; } else if (type === 'couchdb') { const configuration: DatabaseConfiguration = { privatePort: 5984, @@ -895,15 +957,15 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data } export function isARM(arch: string) { if (arch === 'arm' || arch === 'arm64') { - return true + return true; } - return false + return false; } export function getDatabaseImage(type: string, arch: string): string { const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); if (found) { if (isARM(arch)) { - return found.baseImageARM || found.baseImage + return found.baseImageARM || found.baseImage; } return found.baseImage; } @@ -914,14 +976,13 @@ export function getDatabaseVersions(type: string, arch: string): string[] { const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); if (found) { if (isARM(arch)) { - return found.versionsARM || found.versions + return found.versionsARM || found.versions; } return found.versions; } return []; } - export type ComposeFile = { version: ComposerFileVersion; services: Record; @@ -943,11 +1004,13 @@ export type ComposeFileService = { depends_on?: string[]; command?: string; ports?: string[]; - build?: { - context: string; - dockerfile: string; - args?: Record; - } | string; + build?: + | { + context: string; + dockerfile: string; + args?: Record; + } + | string; deploy?: { restart_policy?: { condition?: string; @@ -1016,7 +1079,7 @@ export const createDirectories = async ({ let workdirFound = false; try { workdirFound = !!(await fs.stat(workdir)); - } catch (error) { } + } catch (error) {} if (workdirFound) { await asyncExecShell(`rm -fr ${workdir}`); } @@ -1027,10 +1090,7 @@ export const createDirectories = async ({ }; }; - -export async function stopDatabaseContainer( - database: any -): Promise { +export async function stopDatabaseContainer(database: any): Promise { let everStarted = false; const { id, @@ -1039,7 +1099,10 @@ export async function stopDatabaseContainer( } = database; if (destinationDockerId) { try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) + const { stdout } = await executeDockerCmd({ + dockerId, + command: `docker inspect --format '{{json .State}}' ${id}` + }); if (stdout) { everStarted = true; @@ -1052,7 +1115,6 @@ export async function stopDatabaseContainer( return everStarted; } - export async function stopTcpHttpProxy( id: string, destinationDocker: any, @@ -1062,22 +1124,19 @@ export async function stopTcpHttpProxy( const { id: dockerId } = destinationDocker; let container = `${id}-${publicPort}`; if (forceName) container = forceName; - const found = await checkContainer({ dockerId, container }); + const { found } = await checkContainer({ dockerId, container }); try { if (found) { return await executeDockerCmd({ dockerId, - command: - `docker stop -t 0 ${container} && docker rm ${container}` + command: `docker stop -t 0 ${container} && docker rm ${container}` }); - } } catch (error) { return error; } } - export async function updatePasswordInDb(database, user, newPassword, isRoot) { const { id, @@ -1095,55 +1154,52 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { await executeDockerCmd({ dockerId, command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` - }) + }); } else if (type === 'mariadb') { await executeDockerCmd({ dockerId, command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` - }) - + }); } else if (type === 'postgresql') { if (isRoot) { await executeDockerCmd({ dockerId, command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"` - }) + }); } else { await executeDockerCmd({ dockerId, command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"` - }) + }); } } else if (type === 'mongodb') { await executeDockerCmd({ dockerId, command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"` - }) - + }); } else if (type === 'redis') { await executeDockerCmd({ dockerId, command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}` - }) - + }); } } } export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) { if (exposePort < 1024 || exposePort > 65535) { - throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } + throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }; } if (configuredPort) { if (configuredPort !== exposePort) { const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { - throw { status: 500, message: `Port ${exposePort} is already in use.` } + throw { status: 500, message: `Port ${exposePort} is already in use.` }; } } } else { const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { - throw { status: 500, message: `Port ${exposePort} is already in use.` } + throw { status: 500, message: `Port ${exposePort} is already in use.` }; } } } @@ -1196,7 +1252,7 @@ export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, r } } export function generateRangeArray(start, end) { - return Array.from({ length: (end - start) }, (v, k) => k + start); + return Array.from({ length: end - start }, (v, k) => k + start); } export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) { const { default: isReachable } = await import('is-port-reachable'); @@ -1284,29 +1340,33 @@ export async function startTraefikTCPProxy( ): Promise<{ stdout: string; stderr: string } | Error> { const { network, id: dockerId, remoteEngine } = destinationDocker; const container = `${id}-${publicPort}`; - const found = await checkContainer({ dockerId, container, remove: true }); + const { found } = await checkContainer({ dockerId, container, remove: true }); const { ipv4, ipv6 } = await listSettings(); let dependentId = id; if (type === 'wordpressftp') dependentId = `${id}-ftp`; - const foundDependentContainer = await checkContainer({ dockerId, container: dependentId, remove: true }); + const foundDependentContainer = await checkContainer({ + dockerId, + container: dependentId, + remove: true + }); try { if (foundDependentContainer && !found) { const { stdout: Config } = await executeDockerCmd({ dockerId, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` - }) + }); const ip = JSON.parse(Config)[0].Gateway; - let traefikUrl = otherTraefikEndpoint + let traefikUrl = otherTraefikEndpoint; if (remoteEngine) { - let ip = null + let ip = null; if (isDev) { - ip = getAPIUrl() + ip = getAPIUrl(); } else { - ip = `http://${ipv4 || ipv6}:3000` + ip = `http://${ipv4 || ipv6}:3000`; } - traefikUrl = `${ip}/webhooks/traefik/other.json` + traefikUrl = `${ip}/webhooks/traefik/other.json`; } const tcpProxy = { version: '3.8', @@ -1342,28 +1402,34 @@ export async function startTraefikTCPProxy( await executeDockerCmd({ dockerId, command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d` - }) + }); await fs.rm(`/tmp/docker-compose-${id}.yaml`); } if (!foundDependentContainer && found) { await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${container} && docker rm ${container}` - }) + }); } } catch (error) { return error; } } -export async function getServiceFromDB({ id, teamId }: { id: string; teamId: string }): Promise { +export async function getServiceFromDB({ + id, + teamId +}: { + id: string; + teamId: string; +}): Promise { const settings = await prisma.setting.findFirst(); const body = await prisma.service.findFirst({ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, include: includeServices }); - let { type } = body - type = fixType(type) + let { type } = body; + type = fixType(type); if (body?.serviceSecret.length > 0) { body.serviceSecret = body.serviceSecret.map((s) => { @@ -1372,7 +1438,7 @@ export async function getServiceFromDB({ id, teamId }: { id: string; teamId: str }); } - body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) } + body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) }; return { ...body, settings }; } @@ -1396,52 +1462,52 @@ export function saveUpdateableFields(type: string, data: any) { const update = {}; if (type && serviceFields[type]) { serviceFields[type].map((k) => { - let temp = data[k.name] + let temp = data[k.name]; if (temp) { if (k.isEncrypted) { - temp = encrypt(temp) + temp = encrypt(temp); } if (k.isLowerCase) { - temp = temp.toLowerCase() + temp = temp.toLowerCase(); } if (k.isNumber) { - temp = Number(temp) + temp = Number(temp); } if (k.isBoolean) { - temp = Boolean(temp) + temp = Boolean(temp); } } if (k.isNumber && temp === '') { - temp = null + temp = null; } - update[k.name] = temp + update[k.name] = temp; }); } - return update + return update; } export function getUpdateableFields(type: string, data: any) { const update = {}; if (type && serviceFields[type]) { serviceFields[type].map((k) => { - let temp = data[k.name] + let temp = data[k.name]; if (temp) { if (k.isEncrypted) { - temp = decrypt(temp) + temp = decrypt(temp); } - update[k.name] = temp + update[k.name] = temp; } - update[k.name] = temp + update[k.name] = temp; }); } - return update + return update; } export function fixType(type) { // Hack to fix the type case sensitivity... if (type === 'plausibleanalytics') type = 'plausibleAnalytics'; if (type === 'meilisearch') type = 'meiliSearch'; - return type + return type; } export const getServiceMainPort = (service: string) => { @@ -1452,7 +1518,6 @@ export const getServiceMainPort = (service: string) => { return null; }; - export function makeLabelForServices(type) { return [ 'coolify.managed=true', @@ -1461,8 +1526,14 @@ export function makeLabelForServices(type) { `coolify.service.type=${type}` ]; } -export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { - if (message.message) message = message.message +export function errorHandler({ + status = 500, + message = 'Unknown error.' +}: { + status: number; + message: string | any; +}) { + if (message.message) message = message.message; throw { status, message }; } export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { @@ -1483,8 +1554,12 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private export async function stopBuild(buildId, applicationId) { let count = 0; await new Promise(async (resolve, reject) => { - const { destinationDockerId, status } = await prisma.build.findFirst({ where: { id: buildId } }); - const { id: dockerId } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } }); + const { destinationDockerId, status } = await prisma.build.findFirst({ + where: { id: buildId } + }); + const { id: dockerId } = await prisma.destinationDocker.findFirst({ + where: { id: destinationDockerId } + }); const interval = setInterval(async () => { try { if (status === 'failed' || status === 'canceled') { @@ -1494,12 +1569,15 @@ export async function stopBuild(buildId, applicationId) { if (count > 15) { clearInterval(interval); if (scheduler.workers.has('deployApplication')) { - scheduler.workers.get('deployApplication').postMessage('cancel') + scheduler.workers.get('deployApplication').postMessage('cancel'); } await cleanupDB(buildId, applicationId); return reject(new Error('Deployment canceled.')); } - const { stdout: buildContainers } = await executeDockerCmd({ dockerId, command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` }) + const { stdout: buildContainers } = await executeDockerCmd({ + dockerId, + command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` + }); if (buildContainers) { const containersArray = buildContainers.trim().split('\n'); for (const container of containersArray) { @@ -1509,7 +1587,7 @@ export async function stopBuild(buildId, applicationId) { await removeContainer({ id, dockerId }); clearInterval(interval); if (scheduler.workers.has('deployApplication')) { - scheduler.workers.get('deployApplication').postMessage('cancel') + scheduler.workers.get('deployApplication').postMessage('cancel'); } await cleanupDB(buildId, applicationId); return resolve(); @@ -1517,7 +1595,7 @@ export async function stopBuild(buildId, applicationId) { } } count++; - } catch (error) { } + } catch (error) {} }, 100); }); } @@ -1532,38 +1610,44 @@ async function cleanupDB(buildId: string, applicationId: string) { export function convertTolOldVolumeNames(type) { if (type === 'nocodb') { - return 'nc' + return 'nc'; } } 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 -r` }) + 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 -r` }) + await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` }); } - } catch (error) { } + } catch (error) {} if (lowDiskSpace || force) { if (isDev) { if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`); - return + return; } try { - await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }) - } catch (error) { } + await executeDockerCmd({ + dockerId, + command: `docker container prune -f --filter "label=coolify.managed=true"` + }); + } catch (error) {} try { - await executeDockerCmd({ dockerId, command: `docker image prune -f` }) - } catch (error) { } + await executeDockerCmd({ dockerId, command: `docker image prune -f` }); + } catch (error) {} try { - await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }) - } catch (error) { } + await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }); + } catch (error) {} // Cleanup build caches try { - await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }) - } catch (error) { } + await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }); + } catch (error) {} } } @@ -1576,7 +1660,6 @@ export function persistentVolumes(id, persistentStorage, config) { volumeSet.add(volume); } } - } } const volumesArray = Array.from(volumeSet); @@ -1585,21 +1668,21 @@ export function persistentVolumes(id, persistentStorage, config) { return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; }) || []; - let volumes = [...persistentVolume] - if (volumesArray) volumes = [...volumesArray, ...volumes] - const composeVolumes = volumes.length > 0 && volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] - } - }; - }) || [] + let volumes = [...persistentVolume]; + if (volumesArray) volumes = [...volumesArray, ...volumes]; + const composeVolumes = + (volumes.length > 0 && + volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + })) || + []; - const volumeMounts = Object.assign( - {}, - ...composeVolumes - ) || {} - return { volumeMounts } + const volumeMounts = Object.assign({}, ...composeVolumes) || {}; + return { volumeMounts }; } export function defaultComposeConfiguration(network: string): any { return { @@ -1613,25 +1696,29 @@ export function defaultComposeConfiguration(network: string): any { window: '120s' } } - } + }; } export function decryptApplication(application: any) { if (application) { if (application?.gitSource?.githubApp?.clientSecret) { - application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; + 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; + 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; + 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; + 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 + s.value = decrypt(s.value) || null; return s; }); } diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index f0b13d5f1..dff59b7db 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -13,7 +13,7 @@ export function formatLabelsOnDocker(data) { return container }) } -export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise { +export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> { let containerFound = false; try { const { stdout } = await executeDockerCmd({ @@ -21,10 +21,12 @@ export async function checkContainer({ dockerId, container, remove = false }: { command: `docker inspect --format '{{json .State}}' ${container}` }); - + containerFound = true const parsedStdout = JSON.parse(stdout); const status = parsedStdout.Status; const isRunning = status === 'running'; + const isRestarting = status === 'restarting' + const isExited = status === 'exited' if (status === 'created') { await executeDockerCmd({ dockerId, @@ -39,13 +41,23 @@ export async function checkContainer({ dockerId, container, remove = false }: { `docker rm ${container}` }); } - if (isRunning) { - containerFound = true; - } + + return { + found: containerFound, + status: { + isRunning, + isRestarting, + isExited + + } + }; } catch (err) { // Container not found } - return containerFound; + return { + found: false + }; + } export async function isContainerExited(dockerId: string, containerName: string): Promise { diff --git a/apps/api/src/lib/importers/gitlab.ts b/apps/api/src/lib/importers/gitlab.ts index b1784b6b9..35fa3d8ce 100644 --- a/apps/api/src/lib/importers/gitlab.ts +++ b/apps/api/src/lib/importers/gitlab.ts @@ -10,7 +10,8 @@ export default async function ({ branch, buildId, privateSshKey, - customPort + customPort, + forPublic }: { applicationId: string; workdir: string; @@ -21,11 +22,15 @@ export default async function ({ repodir: string; privateSshKey: string; customPort: number; + forPublic: boolean; }): Promise { const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); - await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); - await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); + + if (!forPublic) { + await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); + await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); + } await saveBuildLog({ line: `Cloning ${repository}:${branch} branch.`, @@ -33,9 +38,16 @@ export default async function ({ applicationId }); - await asyncExecShell( - `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + if (forPublic) { + await asyncExecShell( + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` + ); + } else { + await asyncExecShell( + `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` + ); + } + const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); return commit.replace('\n', ''); } diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index 41ce8e0e7..cc6646737 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -5,6 +5,7 @@ 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, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { defaultServiceConfigurations } from '../services'; +import { OnlyId } from '../../types'; export async function startService(request: FastifyRequest) { try { @@ -317,7 +318,7 @@ async function startMinioService(request: FastifyRequest) { destinationDocker, persistentStorage, exposePort, - minio: { rootUser, rootUserPassword }, + minio: { rootUser, rootUserPassword, apiFqdn }, serviceSecret } = service; @@ -336,7 +337,7 @@ async function startMinioService(request: FastifyRequest) { image: `${image}:${version}`, volumes: [`${id}-minio-data:/data`], environmentVariables: { - MINIO_SERVER_URL: fqdn, + MINIO_SERVER_URL: apiFqdn, MINIO_DOMAIN: getDomain(fqdn), MINIO_ROOT_USER: rootUser, MINIO_ROOT_PASSWORD: rootUserPassword, @@ -658,7 +659,7 @@ async function startLanguageToolService(request: FastifyRequest) { [id]: { container_name: id, image: config.n8n.image, - volumes: config.n8n, + volumes: config.n8n.volumes, environment: config.n8n.environmentVariables, labels: makeLabelForServices('n8n'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), @@ -1009,79 +1010,136 @@ async function startUmamiService(request: FastifyRequest) { } const initDbSQL = ` - drop table if exists event; - drop table if exists pageview; - drop table if exists session; - drop table if exists website; - drop table if exists account; - - create table account ( - user_id serial primary key, - username varchar(255) unique not null, - password varchar(60) not null, - is_admin bool not null default false, - created_at timestamp with time zone default current_timestamp, - updated_at timestamp with time zone default current_timestamp - ); - - create table website ( - website_id serial primary key, - website_uuid uuid unique not null, - user_id int not null references account(user_id) on delete cascade, - name varchar(100) not null, - domain varchar(500), - share_id varchar(64) unique, - created_at timestamp with time zone default current_timestamp - ); - - create table session ( - session_id serial primary key, - session_uuid uuid unique not null, - website_id int not null references website(website_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - hostname varchar(100), - browser varchar(20), - os varchar(20), - device varchar(20), - screen varchar(11), - language varchar(35), - country char(2) - ); - - create table pageview ( - view_id serial primary key, - website_id int not null references website(website_id) on delete cascade, - session_id int not null references session(session_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - url varchar(500) not null, - referrer varchar(500) - ); - - create table event ( - event_id serial primary key, - website_id int not null references website(website_id) on delete cascade, - session_id int not null references session(session_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - url varchar(500) not null, - event_type varchar(50) not null, - event_value varchar(50) not null - ); - - create index website_user_id_idx on website(user_id); - - create index session_created_at_idx on session(created_at); - create index session_website_id_idx on session(website_id); - - create index pageview_created_at_idx on pageview(created_at); - create index pageview_website_id_idx on pageview(website_id); - create index pageview_session_id_idx on pageview(session_id); - create index pageview_website_id_created_at_idx on pageview(website_id, created_at); - create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at); - - create index event_created_at_idx on event(created_at); - create index event_website_id_idx on event(website_id); - create index event_session_id_idx on event(session_id); - + -- CreateTable +CREATE TABLE "account" ( + "user_id" SERIAL NOT NULL, + "username" VARCHAR(255) NOT NULL, + "password" VARCHAR(60) NOT NULL, + "is_admin" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "event" ( + "event_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "event_type" VARCHAR(50) NOT NULL, + "event_value" VARCHAR(50) NOT NULL, + + PRIMARY KEY ("event_id") +); + +-- CreateTable +CREATE TABLE "pageview" ( + "view_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "referrer" VARCHAR(500), + + PRIMARY KEY ("view_id") +); + +-- CreateTable +CREATE TABLE "session" ( + "session_id" SERIAL NOT NULL, + "session_uuid" UUID NOT NULL, + "website_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "hostname" VARCHAR(100), + "browser" VARCHAR(20), + "os" VARCHAR(20), + "device" VARCHAR(20), + "screen" VARCHAR(11), + "language" VARCHAR(35), + "country" CHAR(2), + + PRIMARY KEY ("session_id") +); + +-- CreateTable +CREATE TABLE "website" ( + "website_id" SERIAL NOT NULL, + "website_uuid" UUID NOT NULL, + "user_id" INTEGER NOT NULL, + "name" VARCHAR(100) NOT NULL, + "domain" VARCHAR(500), + "share_id" VARCHAR(64), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("website_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); + +-- CreateIndex +CREATE INDEX "event_created_at_idx" ON "event"("created_at"); + +-- CreateIndex +CREATE INDEX "event_session_id_idx" ON "event"("session_id"); + +-- CreateIndex +CREATE INDEX "event_website_id_idx" ON "event"("website_id"); + +-- CreateIndex +CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); + +-- CreateIndex +CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); + +-- CreateIndex +CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); + +-- CreateIndex +CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); + +-- CreateIndex +CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); + +-- CreateIndex +CREATE INDEX "session_created_at_idx" ON "session"("created_at"); + +-- CreateIndex +CREATE INDEX "session_website_id_idx" ON "session"("website_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); + +-- CreateIndex +CREATE INDEX "website_user_id_idx" ON "website"("user_id"); + +-- AddForeignKey +ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( umamiAdminPassword, 10 @@ -1119,7 +1177,6 @@ async function startUmamiService(request: FastifyRequest) { }, volumes: volumeMounts }; - console.log(composeFile) const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await startServiceContainers(destinationDocker.id, composeFileDestination) @@ -1321,10 +1378,6 @@ async function startAppWriteService(request: FastifyRequest) { const teamId = request.user.teamId; const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) - let isStatsEnabled = false - if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) { - isStatsEnabled = true - } const { opensslKeyV1, executorSecret, @@ -1702,50 +1755,48 @@ async function startAppWriteService(request: FastifyRequest) { }, }; - if (isStatsEnabled) { - dockerCompose[id].depends_on.push(`${id}-influxdb`); - dockerCompose[`${id}-usage`] = { - image: `${image}:${version}`, - container_name: `${id}-usage`, - labels: makeLabelForServices('appwrite'), - entrypoint: "usage", - depends_on: [ - `${id}-mariadb`, - `${id}-influxdb`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - } - dockerCompose[`${id}-influxdb`] = { - image: "appwrite/influxdb:1.5.0", - container_name: `${id}-influxdb`, - volumes: [ - `${id}-influxdb:/var/lib/influxdb:rw` - ], - ...defaultComposeConfiguration(network), - } - dockerCompose[`${id}-telegraf`] = { - image: "appwrite/telegraf:1.4.0", - container_name: `${id}-telegraf`, - environment: [ - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - ], - ...defaultComposeConfiguration(network), - } + dockerCompose[id].depends_on.push(`${id}-influxdb`); + dockerCompose[`${id}-usage`] = { + image: `${image}:${version}`, + container_name: `${id}-usage`, + labels: makeLabelForServices('appwrite'), + entrypoint: "usage", + depends_on: [ + `${id}-mariadb`, + `${id}-influxdb`, + ], + environment: [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + } + dockerCompose[`${id}-influxdb`] = { + image: "appwrite/influxdb:1.5.0", + container_name: `${id}-influxdb`, + volumes: [ + `${id}-influxdb:/var/lib/influxdb:rw` + ], + ...defaultComposeConfiguration(network), + } + dockerCompose[`${id}-telegraf`] = { + image: "appwrite/telegraf:1.4.0", + container_name: `${id}-telegraf`, + environment: [ + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + ], + ...defaultComposeConfiguration(network), } const composeFile: any = { @@ -2646,4 +2697,25 @@ async function startGrafanaService(request: FastifyRequest) { } catch ({ status, message }) { return errorHandler({ status, message }) } -} \ No newline at end of file +} + +export async function migrateAppwriteDB(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params + const teamId = request.user.teamId; + const { + destinationDockerId, + destinationDocker, + } = await getServiceFromDB({ id, teamId }); + if (destinationDockerId) { + await executeDockerCmd({ + dockerId: destinationDocker.id, + command: `docker exec ${id} migrate` + }) + return await reply.code(201).send() + } + throw { status: 500, message: 'Could cleanup logs.' } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts index 7539cfc23..9ad5ce783 100644 --- a/apps/api/src/lib/services/supportedVersions.ts +++ b/apps/api/src/lib/services/supportedVersions.ts @@ -1,3 +1,24 @@ +/* + Example of a supported version: +{ + // Name used to identify the service internally + name: 'umami', + // Fancier name to show to the user + fancyName: 'Umami', + // Docker base image for the service + baseImage: 'ghcr.io/mikecao/umami', + // Optional: If there is any dependent image, you should list it here + images: [], + // Usable tags + versions: ['postgresql-latest'], + // Which tag is the recommended + recommendedVersion: 'postgresql-latest', + // Application's default port, Umami listens on 3000 + ports: { + main: 3000 + } + } +*/ export const supportedServiceTypesAndVersions = [ { name: 'plausibleanalytics', @@ -116,7 +137,7 @@ export const supportedServiceTypesAndVersions = [ { name: 'umami', fancyName: 'Umami', - baseImage: 'ghcr.io/mikecao/umami', + baseImage: 'ghcr.io/umami-software/umami', images: ['postgres:12-alpine'], versions: ['postgresql-latest'], recommendedVersion: 'postgresql-latest', @@ -151,8 +172,8 @@ export const supportedServiceTypesAndVersions = [ fancyName: 'Appwrite', baseImage: 'appwrite/appwrite', images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], - versions: ['latest', '0.15.3'], - recommendedVersion: '0.15.3', + versions: ['latest', '1.0','0.15.3'], + recommendedVersion: '1.0', ports: { main: 80 } diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 8a390a3f5..ffb9e64d4 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -5,6 +5,7 @@ import axios from 'axios'; import { FastifyReply } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; +import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; @@ -12,8 +13,9 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; 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 type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types'; import { OnlyId } from '../../../../types'; +import path from 'node:path'; function filterObject(obj, callback) { return Object.fromEntries(Object.entries(obj). @@ -74,14 +76,19 @@ export async function getApplicationStatus(request: FastifyRequest) { const { teamId } = request.user let isRunning = false; let isExited = false; - + let isRestarting = false; const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { - isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); - isExited = await isContainerExited(application.destinationDocker.id, id); + const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting + } } return { isRunning, + isRestarting, isExited, }; } catch ({ status, message }) { @@ -157,7 +164,8 @@ export async function getApplicationFromDB(id: string, teamId: string) { gitSource: { include: { githubApp: true, gitlabApp: true } }, secrets: true, persistentStorage: true, - connectedDatabase: true + connectedDatabase: true, + previewApplication: true } }); if (!application) { @@ -339,10 +347,11 @@ export async function stopPreviewApplication(request: FastifyRequest, reply: if (secrets.length > 0) { secrets.forEach((secret) => { if (pullmergeRequestId) { - if (secret.isPRMRSecret) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { envs.push(`${secret.name}=${secret.value}`); } } else { @@ -463,7 +475,7 @@ export async function stopApplication(request: FastifyRequest, reply: Fa const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { const { id: dockerId } = application.destinationDocker; - const found = await checkContainer({ dockerId, container: id }); + const { found } = await checkContainer({ dockerId, container: id }); if (found) { await removeContainer({ id, dockerId: application.destinationDocker.id }); } @@ -607,7 +619,7 @@ export async function deployApplication(request: FastifyRequest, reply: Fas try { const { id } = request.params let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body - if (isNew) { const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); if (found) { @@ -810,14 +821,24 @@ export async function saveSecret(request: FastifyRequest, reply: Fas }); } } else { - value = encrypt(value.trim()); + if (value) { + value = encrypt(value.trim()); + } const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); if (found) { - await prisma.secret.updateMany({ - where: { applicationId: id, name, isPRMRSecret }, - data: { value, isBuildSecret, isPRMRSecret } - }); + if (!value && isPRMRSecret) { + await prisma.secret.deleteMany({ + where: { applicationId: id, name, isPRMRSecret } + }); + } else { + + await prisma.secret.updateMany({ + where: { applicationId: id, name, isPRMRSecret }, + data: { value, isBuildSecret, isPRMRSecret } + }); + } + } else { await prisma.secret.create({ data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } @@ -884,6 +905,181 @@ export async function deleteStorage(request: FastifyRequest) { } } +export async function restartPreview(request: FastifyRequest, reply: FastifyReply) { + try { + const { id, pullmergeRequestId } = 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, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; + + const envs = [ + `PORT=${port}` + ]; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + 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}-${pullmergeRequestId}' --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}-${pullmergeRequestId}`]: { + image, + container_name: `${applicationId}-${pullmergeRequestId}`, + 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}-${pullmergeRequestId}` }) + await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }) + 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 getPreviewStatus(request: FastifyRequest) { + try { + const { id, pullmergeRequestId } = request.params + const { teamId } = request.user + let isRunning = false; + let isExited = false; + let isRestarting = false; + let isBuilding = false + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting + } + const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } }) + isBuilding = building.length > 0 + } + return { + isBuilding, + isRunning, + isRestarting, + isExited, + }; + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +export async function loadPreviews(request: FastifyRequest) { + try { + const { id } = request.params + const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); + const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) + if (stdout === '') { + throw { status: 500, message: 'No previews found.' } + } + const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application') + + const jsonContainers = containers + .map((container) => + JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) + ) + .filter((container) => { + return container.pullmergeRequestId && container.applicationId === id; + }); + for (const container of jsonContainers) { + const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } }) + if (found.length === 0) { + await prisma.previewApplication.create({ + data: { + pullmergeRequestId: container.pullmergeRequestId, + sourceBranch: container.branch, + customDomain: container.fqdn, + application: { connect: { id: container.applicationId } } + } + }) + } + } + return { + previews: await prisma.previewApplication.findMany({ where: { applicationId: id } }) + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function getPreviews(request: FastifyRequest) { try { const { id } = request.params @@ -899,26 +1095,7 @@ export async function getPreviews(request: FastifyRequest) { const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret); const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret); - const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) - if (stdout === '') { - return { - containers: [], - applicationSecrets: [], - PRMRSecrets: [] - } - } - const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application') - - const jsonContainers = containers - .map((container) => - JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) - ) - .filter((container) => { - return container.pullmergeRequestId && container.applicationId === id; - }); return { - containers: jsonContainers, applicationSecrets: applicationSecrets.sort((a, b) => { return ('' + a.name).localeCompare(b.name); }), @@ -970,7 +1147,7 @@ export async function getApplicationLogs(request: FastifyRequest) { +export async function getBuilds(request: FastifyRequest) { try { const { id } = request.params let { buildId, skip = 0 } = request.query @@ -987,17 +1164,15 @@ export async function getBuildLogs(request: FastifyRequest) { builds = await prisma.build.findMany({ where: { applicationId: id }, orderBy: { createdAt: 'desc' }, - take: 5, - skip + take: 5 + skip }); } - builds = builds.map((build) => { - const updatedAt = day(build.updatedAt).utc(); - build.took = updatedAt.diff(day(build.createdAt)) / 1000; - build.since = updatedAt.fromNow(); - return build; - }); + if (build.status === 'running') { + build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0); + } + return build + }) return { builds, buildCount @@ -1009,22 +1184,49 @@ export async function getBuildLogs(request: FastifyRequest) { export async function getBuildIdLogs(request: FastifyRequest) { try { - const { buildId } = request.params + // TODO: Fluentbit could still hold the logs, so we need to check if the logs are done + const { buildId, id } = request.params let { sequence = 0 } = request.query if (typeof sequence !== 'number') { sequence = Number(sequence) } - let logs = await prisma.buildLog.findMany({ - where: { buildId, time: { gt: sequence } }, - orderBy: { time: 'asc' } - }); + let file = `/app/logs/${id}_buildlog_${buildId}.csv` + if (isDev) { + file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv` + } const data = await prisma.build.findFirst({ where: { id: buildId } }); const createdAt = day(data.createdAt).utc(); + try { + await fs.stat(file) + } catch (error) { + let logs = await prisma.buildLog.findMany({ + where: { buildId, time: { gt: sequence } }, + orderBy: { time: 'asc' } + }); + const data = await prisma.build.findFirst({ where: { id: buildId } }); + const createdAt = day(data.createdAt).utc(); + return { + logs: logs.map(log => { + log.time = Number(log.time) + return log + }), + fromDb: true, + took: day().diff(createdAt) / 1000, + status: data?.status || 'queued' + } + } + let fileLogs = (await fs.readFile(file)).toString() + let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs) + let logs = decryptedLogs.map(log => { + const parsed = { + time: log['field1'], + line: decrypt(log['field2'] + '","' + log['field3']) + } + return parsed + }).filter(log => log.time > sequence) return { - logs: logs.map(log => { - log.time = Number(log.time) - return log - }), + logs, + fromDb: false, took: day().diff(createdAt) / 1000, status: data?.status || 'queued' } diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index d42906066..74370de4d 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -1,8 +1,8 @@ import { FastifyPluginAsync } from 'fastify'; import { OnlyId } from '../../../../types'; -import { cancelDeployment, checkDNS, 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 { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, 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'; +import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { @@ -37,9 +37,12 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.delete('/:id/storages', async (request) => await deleteStorage(request)); fastify.get('/:id/previews', async (request) => await getPreviews(request)); + fastify.post('/:id/previews/load', async (request) => await loadPreviews(request)); + fastify.get('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request)); + fastify.post('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply)); fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); - fastify.get('/:id/logs/build', async (request) => await getBuildLogs(request)); + fastify.get('/:id/logs/build', async (request) => await getBuilds(request)); fastify.get('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request)); fastify.get('/:id/usage', async (request) => await getUsage(request)) diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index e88a79d72..0699518a5 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -89,7 +89,7 @@ export interface GetApplicationLogs extends OnlyId { since: number, } } -export interface GetBuildLogs extends OnlyId { +export interface GetBuilds extends OnlyId { Querystring: { buildId: string skip: number, @@ -97,6 +97,7 @@ export interface GetBuildLogs extends OnlyId { } export interface GetBuildIdLogs { Params: { + id: string, buildId: string }, Querystring: { @@ -126,4 +127,10 @@ export interface StopPreviewApplication extends OnlyId { Body: { pullmergeRequestId: string | null, } +} +export interface RestartPreviewApplication { + Params: { + id: string, + pullmergeRequestId: string | null, + } } \ 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 bb96981cf..e4f038881 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -229,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', remove: true }) + const { found: isRunning } = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy', remove: true }) return { isRunning } diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index cdb115af3..7b0e9959b 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,13 +1,23 @@ - -import axios from 'axios'; -import { compareVersions } from 'compare-versions'; -import cuid from 'cuid'; -import bcrypt from 'bcryptjs'; -import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; -import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions'; -import type { FastifyReply, FastifyRequest } from 'fastify'; -import type { Login, Update } from '.'; -import type { GetCurrentUser } from './types'; +import axios from "axios"; +import { compareVersions } from "compare-versions"; +import cuid from "cuid"; +import bcrypt from "bcryptjs"; +import { + asyncExecShell, + asyncSleep, + cleanupDockerStorage, + errorHandler, + isDev, + listSettings, + prisma, + uniqueName, + version, +} from "../../../lib/common"; +import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; +import { scheduler } from "../../../lib/scheduler"; +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { Login, Update } from "."; +import type { GetCurrentUser } from "./types"; export async function hashPassword(password: string): Promise { const saltRounds = 15; @@ -17,34 +27,38 @@ export async function hashPassword(password: string): Promise { export async function cleanupManually(request: FastifyRequest) { try { const { serverId } = request.body; - const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) - await cleanupDockerStorage(destination.id, true, true) - return {} + const destination = await prisma.destinationDocker.findUnique({ + where: { id: serverId }, + }); + await cleanupDockerStorage(destination.id, true, true); + return {}; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } export async function checkUpdate(request: FastifyRequest) { try { - const isStaging = request.hostname === 'staging.coolify.io' + const isStaging = + request.hostname === "staging.coolify.io" || + request.hostname === "arm.coolify.io"; const currentVersion = version; const { data: versions } = await axios.get( - `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` + `https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}` ); - const latestVersion = versions['coolify'].main.version + const latestVersion = versions["coolify"].main.version; const isUpdateAvailable = compareVersions(latestVersion, currentVersion); if (isStaging) { return { isUpdateAvailable: true, - latestVersion: 'next' - } + latestVersion: "next", + }; } return { isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, - latestVersion + latestVersion, }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } @@ -59,7 +73,7 @@ export async function update(request: FastifyRequest) { `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .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"` + `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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` ); return {}; } else { @@ -67,13 +81,27 @@ export async function update(request: FastifyRequest) { return {}; } } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); + } +} +export async function resetQueue(request: FastifyRequest) { + try { + const teamId = request.user.teamId; + if (teamId === "0") { + await prisma.build.updateMany({ + where: { status: { in: ["queued", "running"] } }, + data: { status: "canceled" }, + }); + scheduler.workers.get("deployApplication").postMessage("cancel"); + } + } catch ({ status, message }) { + return errorHandler({ status, message }); } } export async function restartCoolify(request: FastifyRequest) { try { const teamId = request.user.teamId; - if (teamId === '0') { + if (teamId === "0") { if (!isDev) { asyncExecShell(`docker restart coolify`); return {}; @@ -81,9 +109,12 @@ export async function restartCoolify(request: FastifyRequest) { return {}; } } - throw { status: 500, message: 'You are not authorized to restart Coolify.' }; + throw { + status: 500, + message: "You are not authorized to restart Coolify.", + }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } @@ -92,24 +123,24 @@ export async function showDashboard(request: FastifyRequest) { const userId = request.user.userId; const teamId = request.user.teamId; const applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, }); const databases = await prisma.database.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, }); const services = await prisma.service.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { destinationDocker: true, teams: true }, }); const gitSources = await prisma.gitSource.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, }); const destinations = await prisma.destinationDocker.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, }); const settings = await listSettings(); return { @@ -121,88 +152,98 @@ export async function showDashboard(request: FastifyRequest) { settings, }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } -export async function login(request: FastifyRequest, reply: FastifyReply) { +export async function login( + request: FastifyRequest, + reply: FastifyReply +) { if (request.user) { - return reply.redirect('/dashboard'); + return reply.redirect("/dashboard"); } else { const { email, password, isLogin } = request.body || {}; if (!email || !password) { - throw { status: 500, message: 'Email and password are required.' }; + throw { status: 500, message: "Email and password are required." }; } const users = await prisma.user.count(); const userFound = await prisma.user.findUnique({ where: { email }, include: { teams: true, permission: true }, - rejectOnNotFound: false + rejectOnNotFound: false, }); if (!userFound && isLogin) { - throw { status: 500, message: 'User not found.' }; + throw { status: 500, message: "User not found." }; } - const { isRegistrationEnabled, id } = await prisma.setting.findFirst() + const { isRegistrationEnabled, id } = await prisma.setting.findFirst(); let uid = cuid(); - let permission = 'read'; + let permission = "read"; let isAdmin = false; if (users === 0) { - await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); - uid = '0'; + await prisma.setting.update({ + where: { id }, + data: { isRegistrationEnabled: false }, + }); + uid = "0"; } if (userFound) { - if (userFound.type === 'email') { - if (userFound.password === 'RESETME') { + if (userFound.type === "email") { + if (userFound.password === "RESETME") { const hashedPassword = await hashPassword(password); if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { - if (userFound.id === '0') { + if (userFound.id === "0") { await prisma.user.update({ where: { email: userFound.email }, - data: { password: 'RESETME' } + data: { password: "RESETME" }, }); } else { await prisma.user.update({ where: { email: userFound.email }, - data: { password: 'RESETTIMEOUT' } + data: { password: "RESETTIMEOUT" }, }); } throw { status: 500, - message: 'Password reset link has expired. Please request a new one.' + message: + "Password reset link has expired. Please request a new one.", }; } else { await prisma.user.update({ where: { email: userFound.email }, - data: { password: hashedPassword } + data: { password: hashedPassword }, }); return { userId: userFound.id, teamId: userFound.id, permission: userFound.permission, - isAdmin: true + isAdmin: true, }; } } - const passwordMatch = await bcrypt.compare(password, userFound.password); + const passwordMatch = await bcrypt.compare( + password, + userFound.password + ); if (!passwordMatch) { throw { status: 500, - message: 'Wrong password or email address.' + message: "Wrong password or email address.", }; } uid = userFound.id; isAdmin = true; } } else { - permission = 'owner'; + permission = "owner"; isAdmin = true; if (!isRegistrationEnabled) { throw { status: 404, - message: 'Registration disabled by administrator.' + message: "Registration disabled by administrator.", }; } const hashedPassword = await hashPassword(password); @@ -212,17 +253,17 @@ export async function login(request: FastifyRequest, reply: FastifyReply) id: uid, email, password: hashedPassword, - type: 'email', + type: "email", teams: { create: { id: uid, name: uniqueName(), - destinationDocker: { connect: { network: 'coolify' } } - } + destinationDocker: { connect: { network: "coolify" } }, + }, }, - permission: { create: { teamId: uid, permission: 'owner' } } + permission: { create: { teamId: uid, permission: "owner" } }, }, - include: { teams: true } + include: { teams: true }, }); } else { await prisma.user.create({ @@ -230,16 +271,16 @@ export async function login(request: FastifyRequest, reply: FastifyReply) id: uid, email, password: hashedPassword, - type: 'email', + type: "email", teams: { create: { id: uid, - name: uniqueName() - } + name: uniqueName(), + }, }, - permission: { create: { teamId: uid, permission: 'owner' } } + permission: { create: { teamId: uid, permission: "owner" } }, }, - include: { teams: true } + include: { teams: true }, }); } } @@ -247,18 +288,21 @@ export async function login(request: FastifyRequest, reply: FastifyReply) userId: uid, teamId: uid, permission, - isAdmin + isAdmin, }; } } -export async function getCurrentUser(request: FastifyRequest, fastify) { - let token = null - const { teamId } = request.query +export async function getCurrentUser( + request: FastifyRequest, + fastify +) { + let token = null; + const { teamId } = request.query; try { const user = await prisma.user.findUnique({ - where: { id: request.user.userId } - }) + where: { id: request.user.userId }, + }); if (!user) { throw "User not found"; } @@ -269,20 +313,20 @@ export async function getCurrentUser(request: FastifyRequest, fa try { const user = await prisma.user.findFirst({ where: { id: request.user.userId, teams: { some: { id: teamId } } }, - include: { teams: true, permission: true } - }) + include: { teams: true, permission: true }, + }); if (user) { - const permission = user.permission.find(p => p.teamId === teamId).permission + const permission = user.permission.find( + (p) => p.teamId === teamId + ).permission; const payload = { ...request.user, teamId, permission: permission || null, - isAdmin: permission === 'owner' || permission === 'admin' - - } - token = fastify.jwt.sign(payload) + isAdmin: permission === "owner" || permission === "admin", + }; + token = fastify.jwt.sign(payload); } - } catch (error) { // No new token -> not switching teams } @@ -291,6 +335,6 @@ export async function getCurrentUser(request: FastifyRequest, fa settings: await prisma.setting.findFirst(), supportedServiceTypesAndVersions, token, - ...request.user - } + ...request.user, + }; } diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 52310998d..6ec94e479 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from 'fastify'; -import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; +import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { GetCurrentUser } from './types'; export interface Update { @@ -23,9 +23,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await getCurrentUser(request, fastify)); - fastify.get('/undead', { - onRequest: [fastify.authenticate] - }, async function () { + fastify.get('/undead', async function () { return { message: 'nope' }; }); @@ -47,6 +45,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await restartCoolify(request)); + fastify.post('/internal/resetQueue', { + onRequest: [fastify.authenticate] + }, async (request) => await resetQueue(request)); + fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); diff --git a/apps/api/src/routes/api/v1/servers/handlers.ts b/apps/api/src/routes/api/v1/servers/handlers.ts index c917836d2..d9705e9ef 100644 --- a/apps/api/src/routes/api/v1/servers/handlers.ts +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -8,9 +8,7 @@ export async function listServers(request: FastifyRequest) { try { const userId = request.user.userId; const teamId = request.user.teamId; - const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] }) - // const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] }) - + const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }}, distinct: ['remoteIpAddress', 'engine'] }) return { servers } @@ -67,8 +65,7 @@ export async function showUsage(request: FastifyRequest) { const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` }) const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` }) - // const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) - // console.log(cpuUsage) + const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) const parsed: any = parseFromText(stats) return { usage: { @@ -81,8 +78,8 @@ export async function showUsage(request: FastifyRequest) { freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100 }, cpu: { - load: 0, - usage: 0, + load: [0,0,0], + usage: cpuUsage, count: cpus }, disk: { diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index c6f10cbb4..2207841d2 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -43,13 +43,17 @@ export async function getServiceStatus(request: FastifyRequest) { let isRunning = false; let isExited = false - + let isRestarting = false; const service = await getServiceFromDB({ id, teamId }); const { destinationDockerId, settings } = service; if (destinationDockerId) { - isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); - isExited = await isContainerExited(service.destinationDocker.id, id); + const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting + } } return { isRunning, @@ -452,7 +456,7 @@ export async function activatePlausibleUsers(request: FastifyRequest, re if (destinationDockerId) { await executeDockerCmd({ dockerId: destinationDocker.id, - command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'` + command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"` }) return await reply.code(201).send() } @@ -472,7 +476,7 @@ export async function cleanupPlausibleLogs(request: FastifyRequest, repl if (destinationDockerId) { await executeDockerCmd({ dockerId: destinationDocker.id, - command: `docker exec ${id}-clickhouse sh -c "/usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\""` + command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"` }) return await reply.code(201).send() } @@ -554,7 +558,7 @@ export async function activateWordpressFtp(request: FastifyRequest => { fastify.addHook('onRequest', async (request) => { @@ -76,6 +76,8 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply)); fastify.post('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply)); + + fastify.post('/:id/appwrite/migrate', async (request, reply) => await migrateAppwriteDB(request, reply)); }; export default root; diff --git a/apps/api/src/routes/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index ba15c1d60..5124be0ef 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -1,7 +1,7 @@ import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; -import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common"; +import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common"; import { checkContainer, removeContainer } from "../../../lib/docker"; import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers"; @@ -154,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi if (application.settings.previews) { if (application.destinationDockerId) { - const isRunning = await checkContainer( + const { found: isRunning } = await checkContainer( { dockerId: application.destinationDocker.id, container: application.id @@ -169,10 +169,29 @@ export async function gitHubEvents(request: FastifyRequest): Promi pullmergeRequestAction === 'reopened' || pullmergeRequestAction === 'synchronize' ) { + await prisma.application.update({ where: { id: application.id }, data: { updatedAt: new Date() } }); + let previewApplicationId = undefined + if (pullmergeRequestId) { + const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) + if (foundPreviewApplications.length > 0) { + previewApplicationId = foundPreviewApplications[0].id + } else { + const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://' + const previewApplication = await prisma.previewApplication.create({ + data: { + pullmergeRequestId, + sourceBranch, + customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`, + application: { connect: { id: application.id } } + } + }) + previewApplicationId = previewApplication.id + } + } // if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') { // // Coolify hosted database // if (application.connectedDatabase.databaseId) { @@ -187,6 +206,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi data: { id: buildId, pullmergeRequestId, + previewApplicationId, sourceBranch, applicationId: application.id, destinationDockerId: application.destinationDocker.id, @@ -198,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest): Promi } }); - + return { + message: 'Queued. Thank you!' + }; } else if (pullmergeRequestAction === 'closed') { if (application.destinationDockerId) { const id = `${application.id}-${pullmergeRequestId}`; @@ -206,13 +228,22 @@ export async function gitHubEvents(request: FastifyRequest): Promi 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); + const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) + if (foundPreviewApplications.length > 0) { + for (const preview of foundPreviewApplications) { + await prisma.previewApplication.delete({ where: { id: preview.id } }) } } + return { + message: 'PR closed. Thank you!' + }; + // if (application?.connectedDatabase?.databaseId) { + // const databaseId = application.connectedDatabase.databaseId; + // const database = await prisma.database.findUnique({ where: { id: databaseId } }); + // if (database) { + // await removeBranchDatabase(database, pullmergeRequestId); + // } + // } } } } diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index 58d01182a..8540c5038 100644 --- a/apps/api/src/routes/webhooks/gitlab/handlers.ts +++ b/apps/api/src/routes/webhooks/gitlab/handlers.ts @@ -2,7 +2,7 @@ import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; import type { FastifyReply, FastifyRequest } from "fastify"; -import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common"; +import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common"; import { checkContainer, removeContainer } from "../../../lib/docker"; import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; @@ -91,8 +91,8 @@ export async function gitLabEvents(request: FastifyRequest) { } } } 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 - + const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body + const pullmergeRequestId = request.body.object_attributes.iid.toString(); const projectId = Number(id); if (!allowedActions.includes(action)) { throw { status: 500, message: 'Action not allowed.' } @@ -107,7 +107,7 @@ export async function gitLabEvents(request: FastifyRequest) { const buildId = cuid(); if (application.settings.previews) { if (application.destinationDockerId) { - const isRunning = await checkContainer( + const { found: isRunning } = await checkContainer( { dockerId: application.destinationDocker.id, container: application.id @@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest) { where: { id: application.id }, data: { updatedAt: new Date() } }); + let previewApplicationId = undefined + if (pullmergeRequestId) { + const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) + if (foundPreviewApplications.length > 0) { + previewApplicationId = foundPreviewApplications[0].id + } else { + const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://' + const previewApplication = await prisma.previewApplication.create({ + data: { + pullmergeRequestId, + sourceBranch, + customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`, + application: { connect: { id: application.id } } + } + }) + previewApplicationId = previewApplication.id + } + } await prisma.build.create({ data: { id: buildId, - pullmergeRequestId: pullmergeRequestId.toString(), + pullmergeRequestId, + previewApplicationId, sourceBranch, applicationId: application.id, destinationDockerId: application.destinationDocker.id, @@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest) { } else if (action === 'close') { if (application.destinationDockerId) { const id = `${application.id}-${pullmergeRequestId}`; - await removeContainer({ id, dockerId: application.destinationDocker.id }); + try { + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } catch (error) { } } + const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) + if (foundPreviewApplications.length > 0) { + for (const preview of foundPreviewApplications) { + await prisma.previewApplication.delete({ where: { id: preview.id } }) + } + } + return { + message: 'MR closed. Thank you!' + }; } } diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index 3e2cd5952..d79c2cb01 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -12,7 +12,7 @@ function configureMiddleware( if (isHttps) { traefik.http.routers[id] = { entrypoints: ['web'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, service: `${id}`, middlewares: ['redirect-to-https'] }; @@ -53,7 +53,7 @@ function configureMiddleware( if (isDualCerts) { traefik.http.routers[`${id}-secure`] = { entrypoints: ['websecure'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, service: `${id}`, tls: { certresolver: 'letsencrypt' @@ -64,7 +64,7 @@ function configureMiddleware( if (isWWW) { traefik.http.routers[`${id}-secure-www`] = { entrypoints: ['websecure'], - rule: `Host(\`www.${nakedDomain}\`)`, + rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, service: `${id}`, tls: { certresolver: 'letsencrypt' @@ -73,7 +73,7 @@ function configureMiddleware( }; traefik.http.routers[`${id}-secure`] = { entrypoints: ['websecure'], - rule: `Host(\`${nakedDomain}\`)`, + rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`/\`)`, service: `${id}`, tls: { domains: { @@ -86,7 +86,7 @@ function configureMiddleware( } else { traefik.http.routers[`${id}-secure-www`] = { entrypoints: ['websecure'], - rule: `Host(\`www.${nakedDomain}\`)`, + rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, service: `${id}`, tls: { domains: { @@ -97,7 +97,7 @@ function configureMiddleware( }; traefik.http.routers[`${id}-secure`] = { entrypoints: ['websecure'], - rule: `Host(\`${domain}\`)`, + rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`, service: `${id}`, tls: { certresolver: 'letsencrypt' @@ -110,14 +110,14 @@ function configureMiddleware( } else { traefik.http.routers[id] = { entrypoints: ['web'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, service: `${id}`, middlewares: [] }; traefik.http.routers[`${id}-secure`] = { entrypoints: ['websecure'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, service: `${id}`, tls: { domains: { diff --git a/apps/ui/package.json b/apps/ui/package.json index caf05c233..74e232854 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -42,6 +42,7 @@ }, "type": "module", "dependencies": { + "dayjs": "1.11.5", "@sveltejs/adapter-static": "1.0.0-next.39", "@tailwindcss/typography": "^0.5.7", "cuid": "2.1.8", diff --git a/apps/ui/src/lib/api.ts b/apps/ui/src/lib/api.ts index 697a29338..1520f863c 100644 --- a/apps/ui/src/lib/api.ts +++ b/apps/ui/src/lib/api.ts @@ -3,33 +3,35 @@ import Cookies from 'js-cookie'; export function getAPIUrl() { if (GITPOD_WORKSPACE_URL) { - const { href } = new URL(GITPOD_WORKSPACE_URL) - const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') - return newURL + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + return newURL; } if (CODESANDBOX_HOST) { - return `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` + return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; } - return dev ? 'http://localhost:3001' : 'http://localhost:3000'; + return dev + ? 'http://localhost:3001' + : 'http://localhost:3000'; } export function getWebhookUrl(type: string) { if (GITPOD_WORKSPACE_URL) { - const { href } = new URL(GITPOD_WORKSPACE_URL) - const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); if (type === 'github') { - return `${newURL}/webhooks/github/events` + return `${newURL}/webhooks/github/events`; } if (type === 'gitlab') { - return `${newURL}/webhooks/gitlab/events` + return `${newURL}/webhooks/gitlab/events`; } } if (CODESANDBOX_HOST) { - const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; if (type === 'github') { - return `${newURL}/webhooks/github/events` + return `${newURL}/webhooks/github/events`; } if (type === 'gitlab') { - return `${newURL}/webhooks/gitlab/events` + return `${newURL}/webhooks/gitlab/events`; } } return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; @@ -103,7 +105,11 @@ async function send({ return {}; } if (!response.ok) { - if (response.status === 401 && !path.startsWith('https://api.github') && !path.includes('/v4/user')) { + if ( + response.status === 401 && + !path.startsWith('https://api.github') && + !path.includes('/v4/user') + ) { Cookies.remove('token'); } diff --git a/apps/ui/src/lib/common.ts b/apps/ui/src/lib/common.ts index 4af26c100..340fa37d1 100644 --- a/apps/ui/src/lib/common.ts +++ b/apps/ui/src/lib/common.ts @@ -83,4 +83,8 @@ export function handlerNotFoundLoad(error: any, url: URL) { status: 500, error: new Error(`Could not load ${url}`) }; +} + +export function getRndInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; } \ No newline at end of file diff --git a/apps/ui/src/lib/components/CopyPasswordField.svelte b/apps/ui/src/lib/components/CopyPasswordField.svelte index 0a6fba471..14a3ca376 100644 --- a/apps/ui/src/lib/components/CopyPasswordField.svelte +++ b/apps/ui/src/lib/components/CopyPasswordField.svelte @@ -13,8 +13,9 @@ export let id: string; export let name: string; export let placeholder = ''; + export let inputStyle = ''; - let disabledClass = 'bg-coolback disabled:bg-coolblack'; + let disabledClass = 'bg-coolback disabled:bg-coolblack w-full'; let isHttps = browser && window.location.protocol === 'https:'; function copyToClipboard() { @@ -32,6 +33,7 @@ {#if !isPasswordField || showPassword} {#if textarea}