feat: docker compose support

This commit is contained in:
Andras Bacsai 2022-10-06 10:25:41 +02:00
parent d8206c0e3e
commit d27426fd8f
5 changed files with 263 additions and 68 deletions

View File

@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
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<SaveApplication>,
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<OnlyId>, 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 });

View File

@ -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<OnlyId>
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<OnlyId>
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts
isDualCerts: dualCerts,
isCustomSSL
});
}
if (previews) {
@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
isDualCerts: dualCerts,
isCustomSSL
});
}
}

View File

@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable<boolean> = 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<any> = writable({
application: {
isRunning: false,
isExited: false,
isRestarting: false,
statuses: [],
overallStatus: 'degraded',
loading: false,
initialLoading: true
},

View File

@ -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 @@
<div>Configurations</div>
<div
class="badge rounded uppercase"
class:text-green-500={$status.application.isRunning}
class:text-red-500={!$status.application.isRunning}
class:text-green-500={$status.application.overallStatus === 'healthy'}
class:text-yellow-400={$status.application.overallStatus === 'degraded'}
class:text-red-500={$status.application.overallStatus === 'stopped'}
>
{$status.application.isRunning ? 'Running' : 'Stopped'}
{$status.application.overallStatus === 'healthy'
? 'Running'
: $status.application.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
</div>
{/if}
@ -245,7 +270,7 @@
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.application.isExited || $status.application.isRestarting}
{#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'}
<a
id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
@ -293,7 +318,7 @@
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.application.isRunning}
{:else if $status.application.overallStatus === 'healthy'}
<button
id="stop"
on:click={stopApplication}
@ -385,11 +410,11 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
{$status.application.overallStatus === 'degraded' ? 'Restart Degraded Services' : 'Deploy'}
</button>
{/if}
{#if $location && $status.application.isRunning}
{#if $location && $status.application.overallStatus === 'healthy'}
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
><svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -28,6 +28,8 @@
<script lang="ts">
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';
}
</script>
<div class="w-full">
@ -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 <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
disabled={$status.application.isRunning}
disabled={isDisabled}
/>
</div>
{/if}
@ -510,12 +539,12 @@
<Setting
id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={$status.application.isRunning}
disabled={isDisabled}
isCenter={false}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description="Generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
on:click={() => !isDisabled && changeSettings('dualCerts')}
/>
</div>
{#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 @@
<div class="grid grid-cols-2 items-center pt-4">
<label for="port"
>{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label
<Explainer
explanation={'The port your application listens inside the docker container.'}
/></label
>
<input
class="w-full"
@ -726,7 +757,7 @@
>
<input
class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning}
readonly={!isDisabled}
disabled={isDisabled}
name="exposePort"
id="exposePort"
@ -884,23 +915,29 @@
<button
class="btn btn-sm btn-primary"
on:click|preventDefault={reloadCompose}
class:loading
disabled={loading}>Reload Docker Compose File</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2">
{#each dockerComposeServices as service}
<div class="grid items-center mb-6">
<div class="text-xl font-bold uppercase">{service.name}</div>
{#if service.data?.image}
<div class="text-xs">{service.data.image}</div>
{:else}
<div class="text-xs">No image, build required</div>
{/if}
<div class="grid items-center bg-coolgray-100 rounded border border-coolgray-300 p-2 px-4">
<div class="text-xl font-bold uppercase">
{service.name}
<span
class="badge rounded text-white"
class:text-red-500={statues[service.name] === 'Exited' ||
statues[service.name] === 'Stopped'}
class:text-yellow-400={statues[service.name] === 'Restarting'}
class:text-green-500={statues[service.name] === 'Running'}
>{statues[service.name] || 'Loading...'}</span
>
</div>
<div class="text-xs">{application.id}-{service.name}</div>
</div>
<div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-2 items-center px-8">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer
@ -910,6 +947,8 @@
<div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="fqdn"
id="fqdn"
bind:value={dockerComposeConfiguration[service.name].fqdn}
@ -918,6 +957,23 @@
/>
</div>
</div>
<div class="grid grid-cols-2 items-center px-8 pb-4">
<label for="port"
>{$t('forms.port')}
<Explainer
explanation={'The port your application listens inside the docker container.'}
/></label
>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="port"
id="port"
bind:value={dockerComposeConfiguration[service.name].port}
placeholder="{$t('forms.default')}: 3000"
/>
</div>
{/each}
</div>
{/if}