From 27af6459b3944e3dff511c219688b3dd9b6913c5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 13 Sep 2022 07:57:57 +0000 Subject: [PATCH 01/34] feat: previewapplications init --- .../migration.sql | 17 +++++++++++ apps/api/prisma/schema.prisma | 13 +++++++++ apps/api/src/jobs/deployApplication.ts | 10 ++++++- .../src/routes/webhooks/github/handlers.ts | 28 +++++++++++++++---- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql diff --git a/apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql b/apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql new file mode 100644 index 000000000..e3b4e7bc7 --- /dev/null +++ b/apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql @@ -0,0 +1,17 @@ +-- AlterTable +ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT; + +-- CreateTable +CREATE TABLE "PreviewApplication" ( + "id" TEXT NOT NULL PRIMARY KEY, + "prMrId" 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..c900d901b 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -119,6 +119,18 @@ model Application { secrets Secret[] teams Team[] connectedDatabase ApplicationConnectedDatabase? + previewApplication PreviewApplication[] +} + +model PreviewApplication { + id String @id @default(cuid()) + prMrId 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 { @@ -219,6 +231,7 @@ model Build { gitlabAppId String? commit String? pullmergeRequestId String? + previewApplicationId String? forceRebuild Boolean @default(false) sourceBranch String? branch String? diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 23adbf608..5909d895b 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, prMrId: 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 }); diff --git a/apps/api/src/routes/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index 4bfea692c..56d9b92a0 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -169,10 +169,21 @@ 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, prMrId: pullmergeRequestId } }) + if (foundPreviewApplications.length > 0) { + previewApplicationId = foundPreviewApplications[0].id + } else { + const previewApplication = await prisma.previewApplication.create({ data: { prMrId: pullmergeRequestId, application: { connect: { id: application.id } } } }) + previewApplicationId = previewApplication.id + } + } // if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') { // // Coolify hosted database // if (application.connectedDatabase.databaseId) { @@ -187,6 +198,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi data: { id: buildId, pullmergeRequestId, + previewApplicationId, sourceBranch, applicationId: application.id, destinationDockerId: application.destinationDocker.id, @@ -206,13 +218,19 @@ 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, prMrId: pullmergeRequestId}}) + if (foundPreviewApplications.length > 0) { + for (const preview of foundPreviewApplications) { + await prisma.previewApplication.delete({where: {id: preview.id}}) } } + // if (application?.connectedDatabase?.databaseId) { + // const databaseId = application.connectedDatabase.databaseId; + // const database = await prisma.database.findUnique({ where: { id: databaseId } }); + // if (database) { + // await removeBranchDatabase(database, pullmergeRequestId); + // } + // } } } } From 18e769b5e51fb2ff020c5b95094a9b489b57cf02 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 13 Sep 2022 10:14:15 +0200 Subject: [PATCH 02/34] fix: plausible analytics actions --- apps/api/src/routes/api/v1/services/handlers.ts | 4 ++-- apps/ui/src/routes/__layout.svelte | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 48e58ca78..2207841d2 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -456,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() } @@ -476,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() } diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index 8694dfe45..071c9f75d 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -219,6 +219,8 @@ + IAM + + Settings
+ Logout +
@@ -287,8 +294,3 @@
- -IAM -Settings -Logout From 8f1e352bcca66acd03313eb5ce6addb013a122e7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 13 Sep 2022 10:14:25 +0200 Subject: [PATCH 03/34] ui: fix plausible --- .../[id]/_Services/_PlausibleAnalytics.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte b/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte index e1992f56d..fee1d8062 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte @@ -20,7 +20,9 @@ name="scriptName" id="scriptName" readonly={!$appSession.isAdmin && !$status.service.isRunning} - disabled={!$appSession.isAdmin || $status.service.isRunning} + disabled={!$appSession.isAdmin || + $status.service.isRunning || + $status.service.initialLoading} placeholder="plausible.js" bind:value={service.plausibleAnalytics.scriptName} required @@ -31,7 +33,9 @@ Date: Tue, 13 Sep 2022 10:14:31 +0200 Subject: [PATCH 04/34] fix: login --- apps/ui/src/routes/login.svelte | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/ui/src/routes/login.svelte b/apps/ui/src/routes/login.svelte index 0fe0e6193..ab1799c19 100644 --- a/apps/ui/src/routes/login.svelte +++ b/apps/ui/src/routes/login.svelte @@ -28,11 +28,7 @@ Cookies.set('token', token, { path: '/' }); - $appSession.teamId = payload.teamId; - $appSession.userId = payload.userId; - $appSession.permission = payload.permission; - $appSession.isAdmin = payload.isAdmin; - return await goto('/'); + return window.location.assign('/'); } catch (error) { return errorNotification(error); } finally { From d9908b3d61eb1930de8a71b62641be3fe13ea4ba Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 13 Sep 2022 15:50:20 +0200 Subject: [PATCH 05/34] feat: previewApplications finalized --- .../migration.sql | 3 +- apps/api/prisma/schema.prisma | 51 +-- apps/api/src/jobs/deployApplication.ts | 2 +- apps/api/src/lib/buildPacks/common.ts | 1 - apps/api/src/lib/common.ts | 2 +- .../routes/api/v1/applications/handlers.ts | 231 ++++++++-- .../src/routes/api/v1/applications/index.ts | 7 +- .../src/routes/api/v1/applications/types.ts | 6 + .../src/routes/webhooks/github/handlers.ts | 39 +- .../src/routes/webhooks/gitlab/handlers.ts | 40 +- apps/ui/package.json | 1 + apps/ui/src/lib/common.ts | 4 + apps/ui/src/lib/components/Usage.svelte | 18 +- apps/ui/src/lib/dayjs.ts | 7 + apps/ui/src/routes/__layout.svelte | 2 +- .../routes/applications/[id]/_Secret.svelte | 14 +- .../applications/[id]/logs/build.svelte | 96 ++-- .../routes/applications/[id]/previews.svelte | 222 ---------- .../applications/[id]/previews/index.svelte | 413 ++++++++++++++++++ apps/ui/src/routes/applications/[id]/utils.ts | 2 +- apps/ui/src/routes/index.svelte | 6 +- apps/ui/src/routes/servers/index.svelte | 2 +- package.json | 2 +- pnpm-lock.yaml | 2 + 24 files changed, 805 insertions(+), 368 deletions(-) rename apps/api/prisma/migrations/{20220912142654_preview_applications => 20220913092100_preview_applications}/migration.sql (89%) create mode 100644 apps/ui/src/lib/dayjs.ts delete mode 100644 apps/ui/src/routes/applications/[id]/previews.svelte create mode 100644 apps/ui/src/routes/applications/[id]/previews/index.svelte diff --git a/apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql b/apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql similarity index 89% rename from apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql rename to apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql index e3b4e7bc7..0ec1aafa0 100644 --- a/apps/api/prisma/migrations/20220912142654_preview_applications/migration.sql +++ b/apps/api/prisma/migrations/20220913092100_preview_applications/migration.sql @@ -4,7 +4,8 @@ ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT; -- CreateTable CREATE TABLE "PreviewApplication" ( "id" TEXT NOT NULL PRIMARY KEY, - "prMrId" TEXT NOT NULL, + "pullmergeRequestId" TEXT NOT NULL, + "sourceBranch" TEXT NOT NULL, "isRandomDomain" BOOLEAN NOT NULL DEFAULT false, "customDomain" TEXT, "applicationId" TEXT NOT NULL, diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index c900d901b..a7500539d 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -119,18 +119,19 @@ model Application { secrets Secret[] teams Team[] connectedDatabase ApplicationConnectedDatabase? - previewApplication PreviewApplication[] + previewApplication PreviewApplication[] } model PreviewApplication { - id String @id @default(cuid()) - prMrId String - isRandomDomain Boolean @default(false) - customDomain String? - applicationId String @unique - application Application @relation(fields: [applicationId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + 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 { @@ -222,22 +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? - previewApplicationId 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 5909d895b..547ef83c3 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -42,7 +42,7 @@ import * as buildpacks from '../lib/buildPacks'; application = decryptApplication(application) const originalApplicationId = application.id if (pullmergeRequestId) { - const previewApplications = await prisma.previewApplication.findMany({where: {applicationId: originalApplicationId, prMrId: pullmergeRequestId}}) + const previewApplications = await prisma.previewApplication.findMany({where: {applicationId: originalApplicationId, pullmergeRequestId}}) if (previewApplications.length > 0) { previewApplicationId = previewApplications[0].id } diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 0e188ae36..eed68511a 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -707,7 +707,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) { Dockerfile.push(`RUN ${installCommand}`); } Dockerfile.push(`RUN ${buildCommand}`); - console.log(Dockerfile.join('\n')) await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await buildImage({ ...data, isCache: true }); } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 857bb1910..e51e64791 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -21,7 +21,7 @@ import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.10.3'; +export const version = '3.10.4'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index c2b375c2a..69b30cedf 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -12,7 +12,7 @@ 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, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication } from './types'; import { OnlyId } from '../../../../types'; function filterObject(obj, callback) { @@ -83,8 +83,6 @@ export async function getApplicationStatus(request: FastifyRequest) { isExited = status.status.isExited; isRestarting = status.status.isRestarting } - - // isExited = await isContainerExited(application.destinationDocker.id, id); } return { isRunning, @@ -164,7 +162,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) { @@ -350,6 +349,7 @@ export async function stopPreviewApplication(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) { @@ -820,14 +819,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 } } } @@ -894,6 +903,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 @@ -909,26 +1093,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); }), @@ -1002,12 +1167,6 @@ export async function getBuildLogs(request: FastifyRequest) { }); } - builds = builds.map((build) => { - const updatedAt = day(build.updatedAt).utc(); - build.took = updatedAt.diff(day(build.createdAt)) / 1000; - build.since = updatedAt.fromNow(); - return build; - }); return { builds, buildCount diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index d42906066..3b1f77bda 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, getBuildLogs, getBuildPack, 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, GetBuildLogs, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { @@ -37,6 +37,9 @@ 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)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index e88a79d72..de8165c5b 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -126,4 +126,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/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index 56d9b92a0..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"; @@ -175,15 +175,23 @@ export async function gitHubEvents(request: FastifyRequest): Promi data: { updatedAt: new Date() } }); let previewApplicationId = undefined - if (pullmergeRequestId) { - const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, prMrId: pullmergeRequestId } }) - if (foundPreviewApplications.length > 0) { - previewApplicationId = foundPreviewApplications[0].id - } else { - const previewApplication = await prisma.previewApplication.create({ data: { prMrId: pullmergeRequestId, application: { connect: { id: application.id } } } }) - previewApplicationId = previewApplication.id - } - } + 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) { @@ -210,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}`; @@ -218,12 +228,15 @@ export async function gitHubEvents(request: FastifyRequest): Promi await removeContainer({ id, dockerId: application.destinationDocker.id }); } catch (error) { } } - const foundPreviewApplications = await prisma.previewApplication.findMany({ where: {applicationId: application.id, prMrId: 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}}) + 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 } }); diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index 264344c4a..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.' } @@ -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/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/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/Usage.svelte b/apps/ui/src/lib/components/Usage.svelte index 7d4a82929..5f833fdff 100644 --- a/apps/ui/src/lib/components/Usage.svelte +++ b/apps/ui/src/lib/components/Usage.svelte @@ -108,21 +108,21 @@
Total Memory
-
+
{(usage?.memory?.totalMemMb).toFixed(0)}MB
Used Memory
-
+
{(usage?.memory?.usedMemMb).toFixed(0)}MB
Free Memory
-
+
{(usage?.memory?.freeMemPercentage).toFixed(0)}%
@@ -131,41 +131,41 @@
Total CPU
-
+
{usage?.cpu?.count}
CPU Usage
-
+
{usage?.cpu?.usage}%
Load Average (5,10,30mins)
-
{usage?.cpu?.load}
+
{usage?.cpu?.load}
Total Disk
-
+
{usage?.disk?.totalGb}GB
Used Disk
-
+
{usage?.disk?.usedGb}GB
Free Disk
-
+
{usage?.disk?.freePercentage}%
diff --git a/apps/ui/src/lib/dayjs.ts b/apps/ui/src/lib/dayjs.ts new file mode 100644 index 000000000..9ff5b0a1a --- /dev/null +++ b/apps/ui/src/lib/dayjs.ts @@ -0,0 +1,7 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +dayjs.extend(utc); +dayjs.extend(relativeTime); + +export { dayjs as day }; diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index 071c9f75d..c14cae406 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -290,7 +290,7 @@ {/if} {/if}
-
+
diff --git a/apps/ui/src/routes/applications/[id]/_Secret.svelte b/apps/ui/src/routes/applications/[id]/_Secret.svelte index d44c2396e..a35b953a4 100644 --- a/apps/ui/src/routes/applications/[id]/_Secret.svelte +++ b/apps/ui/src/routes/applications/[id]/_Secret.svelte @@ -5,7 +5,6 @@ export let isNewSecret = false; export let isPRMRSecret = false; export let PRMRSecret: any = {}; - if (isPRMRSecret) value = PRMRSecret.value; import { page } from '$app/stores'; @@ -39,7 +38,15 @@ async function createSecret(isNew: any) { try { - if (!name || !value) return; + if (isNew) { + if (!name || !value) return; + } + if (value === undefined && isPRMRSecret) { + return + } + if (value === '' && !isPRMRSecret) { + throw new Error('Value is required.') + } await saveSecret({ isNew, name, @@ -108,7 +115,6 @@ name={isNewSecret ? 'secretValue' : 'secretValueNew'} isPasswordField={true} bind:value - required placeholder="J$#@UIO%HO#$U%H" /> @@ -130,7 +136,7 @@ class:translate-x-0={!isBuildSecret} >