feat: docker compose

This commit is contained in:
Andras Bacsai 2022-10-06 15:51:08 +02:00
parent 0c4850b91d
commit 9bb125cebd
14 changed files with 227 additions and 473 deletions

View File

@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks';
baseDirectory,
publishDirectory,
dockerFileLocation,
dockerComposeConfiguration,
denoMainFile
} = application
const currentHash = crypto
@ -112,17 +113,6 @@ 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' }
// });
// }
let imageId = applicationId;
let domain = getDomain(fqdn);
const volumes =
@ -138,6 +128,10 @@ import * as buildpacks from '../lib/buildPacks';
repository = sourceRepository || repository;
}
try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
} catch (error) { }
let deployNeeded = true;
let destinationType;
@ -264,6 +258,7 @@ import * as buildpacks from '../lib/buildPacks';
pythonModule,
pythonVariable,
dockerFileLocation,
dockerComposeConfiguration,
denoMainFile,
denoOptions,
baseImage,

View File

@ -1,296 +0,0 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`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 coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function cleanupPrismaEngines() {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) { }
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}
(async () => {
let status = {
cleanupStorage: false,
autoUpdater: false,
copySSLCertificates: false,
}
if (parentPort) {
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
process.exit(1);
}
if (message === 'action:cleanupStorage') {
if (!status.autoUpdater) {
status.cleanupStorage = true
await cleanupStorage();
status.cleanupStorage = false
}
return;
}
if (message === 'action:cleanupPrismaEngines') {
await cleanupPrismaEngines();
return;
}
if (message === 'action:checkProxies') {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true
await autoUpdater();
status.autoUpdater = false
}
return;
}
}
});
} else process.exit(0);
})();

View File

@ -16,7 +16,8 @@ export default async function (data) {
baseDirectory,
secrets,
pullmergeRequestId,
port
port,
dockerComposeConfiguration
} = data
const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`;
const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`;
@ -76,6 +77,9 @@ export default async function (data) {
value['env_file'] = envFound ? [`${workdir}/.env`] : []
value['labels'] = labels
value['volumes'] = volumes
if (dockerComposeConfiguration[key].port) {
value['expose'] = [dockerComposeConfiguration[key].port]
}
if (value['networks']?.length > 0) {
value['networks'].forEach((network) => {
networks[network] = {

View File

@ -1223,7 +1223,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try {
const { id } = request.params;
const { id, containerId } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
@ -1234,10 +1234,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
});
if (destinationDockerId) {
try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)

View File

@ -45,7 +45,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
// fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(request));
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));

View File

@ -87,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
path: string,
}
}
export interface GetApplicationLogs extends OnlyId {
export interface GetApplicationLogs {
Params: {
id: string,
containerId: string
}
Querystring: {
since: number,
}

View File

@ -18,7 +18,7 @@
<div class="dropdown dropdown-bottom">
<slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-52">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
@ -37,7 +37,7 @@
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
<li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
<button on:click={newApplication} class="no-underline hover:bg-applications tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -58,7 +58,7 @@
>
</li>
<li>
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
<button on:click={newService} class="no-underline hover:bg-services tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -75,7 +75,7 @@
>
</li>
<li>
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
<button on:click={newDatabase} class="no-underline hover:bg-databases tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -94,7 +94,7 @@
>
</li>
<li>
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
<a href="/sources/new" class="no-underline hover:bg-sources tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -116,7 +116,7 @@
>
</li>
<li>
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
<a href="/destinations/new" class="no-underline hover:bg-destinations tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@ -160,12 +160,12 @@
<span>Logs</span>
</li>
<li
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
@ -218,10 +218,10 @@
</li>
<li
class="rounded"
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
<a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"

View File

@ -153,10 +153,16 @@
const data = await get(`/applications/${id}/status`);
$status.application.statuses = data;
const numberOfApplications =
application.buildPack === 'compose'
? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length
: 1;
let numberOfApplications = 0;
if (application.dockerComposeConfiguration) {
numberOfApplications =
application.buildPack === 'compose'
? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length
: 1;
} else {
numberOfApplications = 1;
}
if ($status.application.statuses.length === 0) {
$status.application.overallStatus = 'stopped';
} else {
@ -187,9 +193,6 @@
onDestroy(() => {
$status.application.initialLoading = true;
// $status.application.isRunning = false;
// $status.application.isExited = false;
// $status.application.isRestarting = false;
$status.application.loading = false;
$location = null;
$isDeploymentEnabled = false;
@ -197,9 +200,6 @@
});
onMount(async () => {
setLocation(application, settings);
// $status.application.isRunning = false;
// $status.application.isExited = false;
// $status.application.isRestarting = false;
$status.application.loading = false;
if ($isDeploymentEnabled) {
await getStatus();
@ -321,6 +321,50 @@
Loading...
</button>
{:else if $status.application.overallStatus === 'healthy'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{#if application.buildPack !== 'compose'}
<button
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg> Restart
</button>
{/if}
<button
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
@ -345,51 +389,9 @@
Force Redeploy
</button>
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{#if application.buildPack !== 'compose'}
<button
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg> Restart
</button>
{/if}
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<button
class="btn btn-sm gap-2 btn-primary"
@ -412,9 +414,8 @@
{$status.application.overallStatus === 'degraded' ? 'Restart Degraded Services' : 'Deploy'}
</button>
{/if}
{#if $location && $status.application.overallStatus === 'healthy'}
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -429,9 +430,8 @@
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
</svg>Open</a
>
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
{/if}
</div>
</div>

View File

@ -14,8 +14,9 @@
export let foundConfig: any;
export let scanning: any;
export let packageManager: any;
export let dockerComposeFile: any = null;
export let dockerComposeFile: string | null = null;
export let dockerComposeFileLocation: string | null = null;
export let dockerComposeConfiguration: any = null;
async function handleSubmit(name: string) {
try {
@ -27,11 +28,19 @@
delete tempBuildPack.fancyName;
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
let composeConfiguration: any = {}
if (!dockerComposeConfiguration && dockerComposeFile) {
for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) {
composeConfiguration[name] = {};
}
}
await post(`/applications/${id}`, {
...tempBuildPack,
buildPack: name,
dockerComposeFile,
dockerComposeFileLocation
dockerComposeFileLocation,
dockerComposeConfiguration: JSON.stringify(composeConfiguration) || JSON.stringify({})
});
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
return await goto(from || `/applications/${id}`);

View File

@ -165,7 +165,7 @@
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink}
/>
<button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
<button class="btn bg-orange-600" type="submit">
Load Repository
</button>
</div>

View File

@ -12,6 +12,7 @@
const response = await get(`/applications/${params.id}/configuration/buildpack`);
return {
props: {
application,
...response
}
};
@ -25,6 +26,14 @@
</script>
<script lang="ts">
export let apiUrl: any;
export let projectId: any;
export let repository: any;
export let branch: any;
export let type: any;
export let application: any;
export let isPublicRepository: boolean;
import { onMount } from 'svelte';
import { page } from '$app/stores';
@ -41,16 +50,9 @@
let scanning: boolean = true;
let foundConfig: any = null;
let packageManager: string = 'npm';
let dockerComposeFile: any = null;
let dockerComposeFileLocation: string | null = null;
export let apiUrl: any;
export let projectId: any;
export let repository: any;
export let branch: any;
export let type: any;
export let application: any;
export let isPublicRepository: boolean;
let dockerComposeFile: string | null = application.dockerComposeFile || null;
let dockerComposeFileLocation: string | null = application.dockerComposeFileLocation || null;
let dockerComposeConfiguration: any = application.dockerComposeConfiguration || null;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
@ -224,7 +226,8 @@
);
if (data?.content) {
const content = atob(data.content);
dockerComposeFile = JSON.stringify(yaml.load(content) || null);
const dockerComposeJson = yaml.load(content) || null;
dockerComposeFile = JSON.stringify(dockerComposeJson);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
@ -309,12 +312,7 @@
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
/>
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
@ -331,6 +329,7 @@
bind:foundConfig
{dockerComposeFile}
{dockerComposeFileLocation}
{dockerComposeConfiguration}
/>
</div>
{/each}
@ -341,12 +340,7 @@
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
<div class="p-2">
<BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
/>
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>

View File

@ -59,6 +59,7 @@
$status.application.overallStatus === 'healthy' ||
$status.application.initialLoading;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
let statues: any = {};
let loading = false;
let fqdnEl: any = null;
@ -377,7 +378,7 @@
</script>
<div class="w-full">
<form on:submit|preventDefault={handleSubmit}>
<form on:submit|preventDefault={() => handleSubmit()}>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 ">General</div>
@ -440,7 +441,7 @@
<div class="grid grid-cols-2 items-center">
<label for="buildPack">{$t('application.build_pack')} </label>
{#if isDisabled}
<input class="uppercase w-full" disabled={isDisabled} value={application.buildPack} />
<input class="capitalize w-full" disabled={isDisabled} value={application.buildPack} />
{:else}
<a
href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
@ -914,10 +915,8 @@
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Stack <Beta />
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
on:click|preventDefault={reloadCompose}
disabled={loading}>Reload Docker Compose File</button
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose}
>Reload Docker Compose File</button
>
{/if}
</div>

View File

@ -3,11 +3,11 @@
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { status } from '$lib/store';
import { goto } from '$app/navigation';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
@ -17,26 +17,39 @@
let followingLogs: any;
let logsEl: any;
let position = 0;
if (
!$status.application.isExited &&
!$status.application.isRestarting &&
!$status.application.isRunning
) {
goto(`/applications/${$page.params.id}/`, { replaceState: true });
}
let services: any = [];
let selectedService: any = null;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
if (response.application.dockerComposeFile) {
services = normalizeDockerServices(
JSON.parse(response.application.dockerComposeFile).services
);
} else {
services = [
{
name: ''
}
];
await selectService('')
}
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function loadAllLogs() {
try {
logsLoading = true;
@ -55,7 +68,7 @@
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
@ -89,6 +102,22 @@
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
if (services.length === 1 && init) return
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script>
<div class="mx-auto w-full">
@ -96,50 +125,67 @@
<div class="title font-bold pb-3">Application Logs</div>
</div>
</div>
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
<div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
Container: {application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div>
{/if}