diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 65b0750b2..c5425e25f 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest) { try { const { id } = request.params const { teamId } = request.user - let isRunning = false; - let isExited = false; - let isRestarting = false; + let payload = [] const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { - 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 + if (application.buildPack === 'compose') { + const { stdout: containers } = await executeDockerCmd({ + dockerId: application.destinationDocker.id, + command: + `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const containerObj = JSON.parse(container); + const status = containerObj.State + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; + } + payload.push({ + name: containerObj.Names, + status: { + isRunning, + isExited, + isRestarting + } + }) + } + } + } else { + let isRunning = false; + let isExited = false; + let isRestarting = false; + 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 + payload.push({ + name: id, + status: { + isRunning, + isExited, + isRestarting + } + }) + + } } } - return { - isRunning, - isRestarting, - isExited, - }; + return payload } catch ({ status, message }) { return errorHandler({ status, message }) } @@ -294,7 +335,6 @@ export async function saveApplication(request: FastifyRequest, dockerComposeFileLocation, dockerComposeConfiguration } = request.body - console.log({dockerComposeConfiguration}) if (port) port = Number(port); if (exposePort) { exposePort = Number(exposePort); @@ -515,6 +555,21 @@ export async function stopApplication(request: FastifyRequest, reply: Fa const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { const { id: dockerId } = application.destinationDocker; + if (application.buildPack === 'compose') { + const { stdout: containers } = await executeDockerCmd({ + dockerId: application.destinationDocker.id, + command: + `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + const containerObj = JSON.parse(container); + await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id }); + } + } + return + } const { found } = await checkContainer({ dockerId, container: id }); if (found) { await removeContainer({ id, dockerId: application.destinationDocker.id }); diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index 381869430..e6d4e474a 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -234,6 +234,8 @@ export async function traefikConfiguration(request, reply) { fqdn, id, port, + buildPack, + dockerComposeConfiguration, destinationDocker, destinationDockerId, settings: { previews, dualCerts, isCustomSSL } @@ -241,6 +243,33 @@ export async function traefikConfiguration(request, reply) { if (destinationDockerId) { const { network, id: dockerId } = destinationDocker; const isRunning = true; + if (buildPack === 'compose') { + const services = Object.entries(JSON.parse(dockerComposeConfiguration)) + for (const service of services) { + const [key, value] = service + const { port: customPort, fqdn } = value + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + data.applications.push({ + id: `${id}-${key}`, + container: `${id}-${key}`, + port: customPort ? customPort : port || 3000, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts, + isCustomSSL + }); + } + } + continue; + } + if (fqdn) { const domain = getDomain(fqdn); const nakedDomain = domain.replace(/^www\./, ''); @@ -604,13 +633,41 @@ export async function remoteTraefikConfiguration(request: FastifyRequest fqdn, id, port, + buildPack, + dockerComposeConfiguration, destinationDocker, destinationDockerId, - settings: { previews, dualCerts } + settings: { previews, dualCerts, isCustomSSL } } = application; if (destinationDockerId) { const { id: dockerId, network } = destinationDocker; const isRunning = true; + if (buildPack === 'compose') { + const services = Object.entries(JSON.parse(dockerComposeConfiguration)) + for (const service of services) { + const [key, value] = service + const { port: customPort, fqdn } = value + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + data.applications.push({ + id: `${id}-${key}`, + container: `${id}-${key}`, + port: customPort ? customPort : port || 3000, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts, + isCustomSSL + }); + } + } + continue; + } if (fqdn) { const domain = getDomain(fqdn); const nakedDomain = domain.replace(/^www\./, ''); @@ -626,7 +683,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest isRunning, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } if (previews) { @@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest nakedDomain, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } } diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index aa3493ca5..0d399a11c 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable = writable(false); export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { return ( isAdmin && + (application.buildPack === 'compose') || (application.fqdn || application.settings.isBot) && application.gitSource && application.repository && @@ -74,9 +75,8 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) } export const status: Writable = writable({ application: { - isRunning: false, - isExited: false, - isRestarting: false, + statuses: [], + overallStatus: 'degraded', loading: false, initialLoading: true }, diff --git a/apps/ui/src/routes/applications/[id]/__layout.svelte b/apps/ui/src/routes/applications/[id]/__layout.svelte index 31aebdf5c..fce55a758 100644 --- a/apps/ui/src/routes/applications/[id]/__layout.svelte +++ b/apps/ui/src/routes/applications/[id]/__layout.svelte @@ -59,7 +59,6 @@ import { goto } from '$app/navigation'; import { onDestroy, onMount } from 'svelte'; import { t } from '$lib/translations'; - import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import { appSession, status, @@ -140,13 +139,11 @@ async function stopApplication() { try { $status.application.initialLoading = true; - // $status.application.loading = true; await post(`/applications/${id}/stop`, {}); } catch (error) { return errorNotification(error); } finally { $status.application.initialLoading = false; - // $status.application.loading = false; await getStatus(); } } @@ -154,18 +151,45 @@ if ($status.application.loading) return; $status.application.loading = true; const data = await get(`/applications/${id}/status`); - $status.application.isRunning = data.isRunning; - $status.application.isExited = data.isExited; - $status.application.isRestarting = data.isRestarting; + + $status.application.statuses = data; + const numberOfApplications = + application.buildPack === 'compose' + ? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length + : 1; + if ($status.application.statuses.length === 0) { + $status.application.overallStatus = 'stopped'; + } else { + if ($status.application.statuses.length !== numberOfApplications) { + $status.application.overallStatus = 'degraded'; + } else { + for (const oneStatus of $status.application.statuses) { + if (oneStatus.status.isExited || oneStatus.status.isRestarting) { + $status.application.overallStatus = 'degraded'; + break; + } + if (oneStatus.status.isRunning) { + $status.application.overallStatus = 'healthy'; + } + if ( + !oneStatus.status.isExited && + !oneStatus.status.isRestarting && + !oneStatus.status.isRunning + ) { + $status.application.overallStatus = 'stopped'; + } + } + } + } $status.application.loading = false; $status.application.initialLoading = false; } onDestroy(() => { $status.application.initialLoading = true; - $status.application.isRunning = false; - $status.application.isExited = false; - $status.application.isRestarting = false; + // $status.application.isRunning = false; + // $status.application.isExited = false; + // $status.application.isRestarting = false; $status.application.loading = false; $location = null; $isDeploymentEnabled = false; @@ -173,15 +197,11 @@ }); onMount(async () => { setLocation(application, settings); - $status.application.isRunning = false; - $status.application.isExited = false; - $status.application.isRestarting = false; + // $status.application.isRunning = false; + // $status.application.isExited = false; + // $status.application.isRestarting = false; $status.application.loading = false; - if ( - application.gitSourceId && - application.destinationDockerId && - (application.fqdn || application.settings.isBot) - ) { + if ($isDeploymentEnabled) { await getStatus(); statusInterval = setInterval(async () => { await getStatus(); @@ -208,10 +228,15 @@
Configurations
- {$status.application.isRunning ? 'Running' : 'Stopped'} + {$status.application.overallStatus === 'healthy' + ? 'Running' + : $status.application.overallStatus === 'degraded' + ? 'Degraded' + : 'Stopped'}
{/if} @@ -245,7 +270,7 @@
- {#if $status.application.isExited || $status.application.isRestarting} + {#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'} - {:else if $status.application.isRunning} + {:else if $status.application.overallStatus === 'healthy'} {/if} - {#if $location && $status.application.isRunning} + {#if $location && $status.application.overallStatus === 'healthy'} export let application: any; export let settings: any; + + import yaml from 'js-yaml'; import { page } from '$app/stores'; import { onMount } from 'svelte'; import Select from 'svelte-select'; @@ -47,13 +49,16 @@ import Setting from '$lib/components/Setting.svelte'; import Explainer from '$lib/components/Explainer.svelte'; import { goto } from '$app/navigation'; - import yaml from 'js-yaml'; const { id } = $page.params; $: isDisabled = - !$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading; + !$appSession.isAdmin || + $status.application.overallStatus === 'degraded' || + $status.application.overallStatus === 'healthy' || + $status.application.initialLoading; + let statues: any = {}; let loading = false; let fqdnEl: any = null; let forceSave = false; @@ -176,7 +181,7 @@ isCustomSSL = !isCustomSSL; } if (name === 'isBot') { - if ($status.application.isRunning) return; + if ($status.application.overallStatus !== 'stopped') return; isBot = !isBot; application.settings.isBot = isBot; application.fqdn = null; @@ -228,9 +233,9 @@ $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); } } - async function handleSubmit() { + async function handleSubmit(toast: boolean = true) { if (loading) return; - loading = true; + if (toast) loading = true; try { nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); if (application.deploymentType) @@ -252,7 +257,7 @@ forceSave = false; - addToast({ + toast && addToast({ message: 'Configuration saved.', type: 'success' }); @@ -333,7 +338,7 @@ let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent); dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services); application.dockerComposeFile = dockerComposeFileContent; - await handleSubmit(); + await handleSubmit(false); } addToast({ message: 'Compose file reloaded.', @@ -343,6 +348,30 @@ errorNotification(error); } } + $: if ($status.application.statuses) { + for (const service of dockerComposeServices) { + getStatus(service); + } + } + function getStatus(service: any) { + let foundStatus = null; + const foundService = $status.application.statuses.find( + (s: any) => s.name === `${application.id}-${service.name}` + ); + if (foundService) { + const statusText = foundService?.status; + if (statusText?.isRunning) { + foundStatus = 'Running'; + } + if (statusText?.isExited) { + foundStatus = 'Exited'; + } + if (statusText?.isRestarting) { + foundStatus = 'Restarting'; + } + } + statues[service.name] = foundStatus || 'Stopped'; + }
@@ -443,7 +472,7 @@ on:click={() => changeSettings('isBot')} title="Is your application a bot?" description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." - disabled={$status.application.isRunning} + disabled={isDisabled} />
{/if} @@ -510,12 +539,12 @@ !$status.application.isRunning && changeSettings('dualCerts')} + on:click={() => !isDisabled && changeSettings('dualCerts')} />
{#if isHttps && application.buildPack !== 'compose'} @@ -552,7 +581,7 @@ {isDisabled} containerClasses={isDisabled && containerClass()} id="baseBuildImages" - showIndicator={!$status.application.isRunning} + showIndicator={!isDisabled} items={application.baseBuildImages} on:select={selectBaseBuildImage} value={application.baseBuildImage} @@ -572,7 +601,7 @@ {isDisabled} containerClasses={isDisabled && containerClass()} id="baseImages" - showIndicator={!$status.application.isRunning} + showIndicator={!isDisabled} items={application.baseImages} on:select={selectBaseImage} value={application.baseImage} @@ -594,7 +623,7 @@ {isDisabled} containerClasses={isDisabled && containerClass()} id="deploymentTypes" - showIndicator={!$status.application.isRunning} + showIndicator={!isDisabled} items={['static', 'node']} on:select={selectDeploymentType} value={application.deploymentType} @@ -705,7 +734,9 @@
Reload Docker Compose File {/if}
{#each dockerComposeServices as service} -
-
{service.name}
- {#if service.data?.image} -
{service.data.image}
- {:else} -
No image, build required
- {/if} +
+
+ {service.name} + {statues[service.name] || 'Loading...'} +
+
{application.id}-{service.name}
+
-
+
+
+ + +
{/each}
{/if}