From 92f513d5147baaebd3d3069bd494b901dfa23048 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 31 Aug 2022 15:03:04 +0200 Subject: [PATCH] feat: restart application --- .../routes/api/v1/applications/handlers.ts | 120 +++- .../src/routes/api/v1/applications/index.ts | 3 +- .../routes/applications/[id]/__layout.svelte | 663 +++++++++--------- 3 files changed, 470 insertions(+), 316 deletions(-) diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index c83504f94..27fbdee7b 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -3,16 +3,23 @@ import crypto from 'node:crypto' import jsonwebtoken from 'jsonwebtoken'; import axios from 'axios'; import { FastifyReply } from 'fastify'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; + import { day } from '../../../../lib/dayjs'; -import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; +import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; -import { scheduler } from '../../../../lib/scheduler'; import type { FastifyRequest } from 'fastify'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types'; import { OnlyId } from '../../../../types'; +function filterObject(obj, callback) { + return Object.fromEntries(Object.entries(obj). + filter(([key, val]) => callback(val, key))); +} + export async function listApplications(request: FastifyRequest) { try { const { teamId } = request.user @@ -312,6 +319,113 @@ export async function stopPreviewApplication(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params + const { teamId } = request.user + let application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; + + const envs = [ + `PORT=${port}` + ]; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + if (secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + const { workdir } = await createDirectories({ repository, buildId }); + const labels = [] + let image = null + const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }) + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`) + } + }) + } + let imageFound = false; + try { + await executeDockerCmd({ + dockerId, + command: `docker image inspect ${image}` + }) + imageFound = true; + } catch (error) { + // + } + if (!imageFound) { + throw { status: 500, message: 'Image not found, cannot restart application.' } + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [applicationId]: { + image, + container_name: applicationId, + volumes, + env_file: envFound ? [`${workdir}/.env`] : [], + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) + await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) + return reply.code(201).send(); + } + throw { status: 500, message: 'Application cannot be restarted.' } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function stopApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index 2f698ddeb..32ac1f98b 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -1,6 +1,6 @@ import { FastifyPluginAsync } from 'fastify'; import { OnlyId } from '../../../../types'; -import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; +import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; @@ -19,6 +19,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/status', async (request) => await getApplicationStatus(request)); + fastify.post('/:id/restart', async (request, reply) => await restartApplication(request, reply)); fastify.post('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); diff --git a/apps/ui/src/routes/applications/[id]/__layout.svelte b/apps/ui/src/routes/applications/[id]/__layout.svelte index 83f66dc6f..4cf80ef4d 100644 --- a/apps/ui/src/routes/applications/[id]/__layout.svelte +++ b/apps/ui/src/routes/applications/[id]/__layout.svelte @@ -62,9 +62,7 @@ import { t } from '$lib/translations'; import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import { errorNotification, handlerNotFoundLoad } from '$lib/common'; - import Loading from '$lib/components/Loading.svelte'; - let loading = false; let statusInterval: any; $disabledButton = !$appSession.isAdmin || @@ -78,7 +76,10 @@ async function handleDeploySubmit(forceRebuild = false) { try { - const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild }); + const { buildId } = await post(`/applications/${id}/deploy`, { + ...application, + forceRebuild + }); addToast({ message: $t('application.deployment_queued'), type: 'success' @@ -98,22 +99,41 @@ async function deleteApplication(name: string) { const sure = confirm($t('application.confirm_to_delete', { name })); if (sure) { - loading = true; + $status.application.initialLoading = true; try { await del(`/applications/${id}`, { id }); return await goto(`/applications`); } catch (error) { return errorNotification(error); + } finally { + $status.application.initialLoading = false; } } } + async function restartApplication() { + try { + $status.application.initialLoading = true; + $status.application.loading = true; + await post(`/applications/${id}/restart`, {}); + } catch (error) { + return errorNotification(error); + } finally { + $status.application.initialLoading = false; + $status.application.loading = false; + await getStatus(); + } + } async function stopApplication() { try { - loading = true; + $status.application.initialLoading = true; + $status.application.loading = true; await post(`/applications/${id}/stop`, {}); - return window.location.reload(); } catch (error) { return errorNotification(error); + } finally { + $status.application.initialLoading = false; + $status.application.loading = false; + await getStatus(); } } async function getStatus() { @@ -152,209 +172,136 @@