wip: trpc

This commit is contained in:
Andras Bacsai 2022-12-21 15:06:33 +01:00
parent e3e39af6fb
commit c8f7ca920e
36 changed files with 1761 additions and 10 deletions

5
.gitignore vendored
View File

@ -1,7 +1,8 @@
.DS_Store
node_modules
.pnpm-store
build
/apps/ui/build
/build
.svelte-kit
package
.env
@ -13,7 +14,7 @@ apps/api/db/migration.db-journal
apps/api/core*
apps/backup/backups/*
!apps/backup/backups/.gitkeep
logs
/logs
others/certificates
backups/*
!backups/.gitkeep

View File

@ -42,6 +42,7 @@
"@trpc/server": "10.1.0",
"cuid": "2.1.8",
"daisyui": "2.41.0",
"dayjs": "1.11.6",
"flowbite-svelte": "0.28.0",
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",

View File

@ -183,3 +183,19 @@ export function put(
): Promise<Record<string, any>> {
return send({ method: 'PUT', path, data, headers });
}
export function changeQueryParams(buildId: string) {
const queryParams = new URLSearchParams(window.location.search);
queryParams.set('buildId', buildId);
// @ts-ignore
return history.pushState(null, null, '?' + queryParams.toString());
}
export const dateOptions: any = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
};

View File

@ -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 };

View File

@ -170,3 +170,4 @@ export const setLocation = (resource: any, settings?: any) => {
disabledButton.set(false);
}
};
export const selectedBuildId: any = writable(null)

View File

@ -11,7 +11,7 @@
import DatabaseIcons from '$lib/components/icons/databases/DatabaseIcons.svelte';
import ServiceIcons from '$lib/components/icons/services/ServiceIcons.svelte';
import * as Icons from '$lib/components/icons';
import NewResource from './_components/NewResource.svelte';
import NewResource from './components/NewResource.svelte';
const {
applications,

View File

@ -3,10 +3,10 @@
import { status, trpc } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import type { LayoutData } from './$types';
import * as Buttons from './_components/Buttons';
import * as States from './_components/States';
import * as Buttons from './components/Buttons';
import * as States from './components/States';
import Menu from './_components/Menu.svelte';
import Menu from './components/Menu.svelte';
export let data: LayoutData;
const id = $page.params.id;

View File

@ -0,0 +1,204 @@
<script lang="ts">
import type { PageData } from '../build/$types';
export let data: PageData;
console.log(data);
let builds = data.builds;
const application = data.application.data;
const buildCount = data.buildCount;
import { page } from '$app/stores';
import { addToast, selectedBuildId, trpc } from '$lib/store';
import BuildLog from './BuildLog.svelte';
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { onDestroy, onMount } from 'svelte';
const { id } = $page.params;
let debug = application.settings.debug;
let loadBuildLogsInterval: any = null;
let skip = 0;
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
let preselectedBuildId = $page.url.searchParams.get('buildId');
if (preselectedBuildId) $selectedBuildId = preselectedBuildId;
onMount(async () => {
getBuildLogs();
loadBuildLogsInterval = setInterval(() => {
getBuildLogs();
}, 2000);
});
onDestroy(() => {
clearInterval(loadBuildLogsInterval);
});
async function getBuildLogs() {
const response = await trpc.applications.getBuilds.query({ id, skip });
builds = response.builds;
}
async function loadMoreBuilds() {
if (buildCount >= skip) {
skip = skip + 5;
noMoreBuilds = buildCount <= skip;
try {
const data = await trpc.applications.getBuilds.query({ id, skip });
builds = data.builds;
return;
} catch (error) {
return errorNotification(error);
}
} else {
noMoreBuilds = true;
}
}
function loadBuild(build: any) {
$selectedBuildId = build;
return changeQueryParams($selectedBuildId);
}
async function resetQueue() {
const sure = confirm(
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
);
if (sure) {
try {
await trpc.applications.resetQueue.mutate();
addToast({
message: 'Queue reset done.',
type: 'success'
});
await asyncSleep(500);
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
function generateBadgeColors(status: string) {
if (status === 'failed') {
return 'text-red-500';
} else if (status === 'running') {
return 'text-yellow-300';
} else if (status === 'success') {
return 'text-green-500';
} else if (status === 'canceled') {
return 'text-orange-500';
} else {
return 'text-white';
}
}
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
try {
trpc.applications.saveSettings.mutate({
id,
debug
});
return addToast({
message: 'Settings saved.',
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
return errorNotification(error);
}
}
</script>
<div class="mx-auto w-full lg:px-0 px-1">
<div class="flex lg:flex-row flex-col border-b border-coolgray-500 mb-6 space-x-2">
<div class="flex flex-row">
<div class="title font-bold pb-3 pr-3">Build Logs</div>
<button class="btn btn-sm bg-error" on:click={resetQueue}>Reset Build Queue</button>
</div>
<div class=" flex-1" />
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text text-white pr-4 font-bold">Enable Debug Logs</span>
<input
type="checkbox"
checked={debug}
class="checkbox checkbox-success"
on:click={() => changeSettings('debug')}
/>
</label>
</div>
</div>
</div>
<div class="justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{:else if buildCount === 0}
Not build logs found.
{:else}
Select a build to see the logs.
{/if}
</div>
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<div class="top-4 md:sticky">
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>Load more</button
>
</div>
{#each builds as build, index (build.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id={`building-${build.id}`}
on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-150 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={$selectedBuildId === build.id}
>
<div class="flex-col px-2 text-center">
<div class="text-sm font-bold truncate">
{build.branch || application.branch}
</div>
<div class="text-xs">
{build.type}
</div>
<div
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
build.status
)}`}
>
{build.status}
</div>
</div>
<div class="w-32 text-center text-xs">
{#if build.status === 'running'}
<div>
<span class="font-bold text-xl">{build.elapsed}s</span>
</div>
{:else if build.status !== 'queued'}
<div>{day(build.updatedAt).utc().fromNow()}</div>
<div>
Finished in
<span class="font-bold"
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
>
</div>
{/if}
</div>
</div>
<Tooltip triggeredBy={`#building-${build.id}`}
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
`\n`}</Tooltip
>
{/each}
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
import { error } from '@sveltejs/kit';
import { trpc } from '$lib/store';
import type { PageLoad } from './$types';
export const ssr = false;
export const load: PageLoad = async ({ params }) => {
try {
const { id } = params;
const data = await trpc.applications.getBuilds.query({ id, skip: 0 });
return data;
} catch (err) {
throw error(500, {
message: 'An unexpected error occurred, please try again later.'
});
}
};

View File

@ -0,0 +1,215 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs';
import { selectedBuildId, trpc } from '$lib/store';
import { dev } from '$app/environment';
let logs: any = [];
let currentStatus: any;
let streamInterval: any;
let followingLogs: any;
let followingInterval: any;
let logsEl: any;
let fromDb = false;
let cancelInprogress = false;
let position = 0;
let loading = true;
const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 100);
} else {
window.clearInterval(followingInterval);
}
}
async function streamLogs(sequence = 0) {
try {
loading = true;
let {
logs: responseLogs,
status,
fromDb: from
} = await trpc.applications.getBuildLogs.query({ id, buildId: $selectedBuildId, sequence });
currentStatus = status;
logs = logs.concat(
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
fromDb = from;
streamInterval = setInterval(async () => {
const nextSequence = logs[logs.length - 1]?.time || 0;
if (status !== 'running' && status !== 'queued') {
loading = false;
try {
const data = await trpc.applications.getBuildLogs.query({
id,
buildId: $selectedBuildId,
sequence: nextSequence
});
status = data.status;
currentStatus = status;
fromDb = data.fromDb;
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
clearInterval(streamInterval);
return;
}
try {
const data = await trpc.applications.getBuildLogs.query({
id,
buildId: $selectedBuildId,
sequence: nextSequence
});
status = data.status;
currentStatus = status;
fromDb = data.fromDb;
logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
);
loading = false;
} catch (error) {
return errorNotification(error);
}
}, 1000);
} catch (error) {
return errorNotification(error);
}
}
async function cancelBuild() {
if (cancelInprogress) return;
try {
cancelInprogress = true;
await trpc.applications.cancelBuild.mutate({
buildId: $selectedBuildId,
applicationId: id
});
} catch (error) {
return errorNotification(error);
}
}
onDestroy(() => {
clearInterval(streamInterval);
clearInterval(followingInterval);
});
onMount(async () => {
window.scrollTo(0, 0);
await streamLogs();
});
</script>
<div class="flex justify-start top-0 pb-2 space-x-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={followingLogs || currentStatus !== 'running'}
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>
<button
on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="btn btn-sm"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={cancelInprogress || currentStatus !== 'running'}
>
<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" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{cancelInprogress ? 'Cancelling...' : 'Cancel Build'}
</button>
{#if currentStatus === 'running'}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
{#if currentStatus === 'queued'}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
Queued and waiting for execution.
</div>
{:else if logs.length > 0}
<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 whitespace-pre"
>
{#each logs as log}
{#if fromDb}
{log.line + '\n'}
{:else}
[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}
{/if}
{/each}
</div>
{:else}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{loading
? 'Loading logs...'
: dev
? 'In development, logs are shown in the console.'
: 'No logs found yet.'}
</div>
{/if}

View File

@ -149,9 +149,9 @@
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs/build`}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/builds`}
>
<a href={`/applications/${$page.params.id}/logs/build`} class="no-underline w-full"
<a href={`/applications/${$page.params.id}/builds`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@ -0,0 +1,176 @@
<script lang="ts">
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
import { trpc } from '$lib/store';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
let followingInterval: any;
let followingLogs: any;
let logsEl: any;
let position = 0;
let services: any = [];
let selectedService: any = null;
let noContainer = false;
const { id } = $page.params;
onMount(async () => {
const { data } = await trpc.applications.getApplicationById.query({ id });
application = data;
if (data.dockerComposeFile) {
services = normalizeDockerServices(JSON.parse(data.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 loadLogs() {
if (logsLoading) return;
try {
const newLogs = await trpc.applications.loadLogs.query({
id,
containerId: selectedService,
since: Number(lastLog?.split(' ')[0]) || 0
});
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
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">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Application Logs</div>
</div>
</div>
<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"
>
{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}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{: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 " class:bg-coollabs={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"
>Streaming logs</button
>
{/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}

View File

@ -9,8 +9,8 @@
import pLimit from 'p-limit';
import { page } from '$app/stores';
import { addToast, trpc } from '$lib/store';
import Secret from './_components/Secret.svelte';
import PreviewSecret from './_components/PreviewSecret.svelte';
import Secret from './components/Secret.svelte';
import PreviewSecret from './components/PreviewSecret.svelte';
import { errorNotification } from '$lib/common';
import Explainer from '$lib/components/Explainer.svelte';

View File

@ -16,17 +16,23 @@
"db:migrate": "DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name"
},
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.6.0",
"@fastify/cors": "8.2.0",
"@fastify/env": "4.1.0",
"@fastify/jwt": "6.5.0",
"@fastify/static": "6.6.0",
"@fastify/websocket": "7.1.1",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.6.1",
"@trpc/client": "10.1.0",
"@trpc/server": "10.1.0",
"abort-controller": "3.0.0",
"axe": "11.0.0",
"bcryptjs": "2.4.3",
"bree": "9.1.2",
"cabin": "11.0.1",
"csvtojson": "2.0.10",
"cuid": "2.1.8",
"dayjs": "1.11.6",
"dotenv": "^16.0.3",
@ -39,9 +45,12 @@
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"node-fetch": "3.3.0",
"p-all": "4.0.0",
"p-throttle": "5.0.0",
"prisma": "4.6.1",
"shell-quote": "^1.7.4",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"superjson": "1.11.0",
"tslib": "2.4.1",
"unique-names-generator": "4.7.1",

View File

@ -0,0 +1,807 @@
import { parentPort } from 'node:worker_threads';
import crypto from 'crypto';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import {
copyBaseConfigurationFiles,
makeLabelForSimpleDockerfile,
makeLabelForStandaloneApplication,
saveBuildLog,
saveDockerRegistryCredentials,
setDefaultConfiguration
} from '../lib/buildPacks/common';
import {
createDirectories,
decrypt,
defaultComposeConfiguration,
getDomain,
prisma,
decryptApplication,
isDev,
pushToRegistry,
executeCommand,
generateSecrets
} from '../lib/common';
import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks';
(async () => {
if (parentPort) {
parentPort.on('message', async (message) => {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
await prisma.$disconnect();
process.exit(0);
}
});
const pThrottle = await import('p-throttle');
const throttle = pThrottle.default({
limit: 1,
interval: 2000
});
const th = throttle(async () => {
try {
const queuedBuilds = await prisma.build.findMany({
where: { status: { in: ['queued', 'running'] } },
orderBy: { createdAt: 'asc' }
});
const { concurrentBuilds } = await prisma.setting.findFirst({});
if (queuedBuilds.length > 0) {
parentPort.postMessage({ deploying: true });
const concurrency = concurrentBuilds;
const pAll = await import('p-all');
const actions = [];
for (const queueBuild of queuedBuilds) {
actions.push(async () => {
let application = await prisma.application.findUnique({
where: { id: queueBuild.applicationId },
include: {
dockerRegistry: true,
destinationDocker: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
persistentStorage: true,
secrets: true,
settings: true,
teams: true
}
});
let {
id: buildId,
type,
gitSourceId,
sourceBranch = null,
pullmergeRequestId = null,
previewApplicationId = null,
forceRebuild,
sourceRepository = null
} = queueBuild;
application = decryptApplication(application);
if (!gitSourceId && application.simpleDockerfile) {
const {
id: applicationId,
destinationDocker,
destinationDockerId,
secrets,
port,
persistentStorage,
exposePort,
simpleDockerfile,
dockerRegistry
} = application;
const { workdir } = await createDirectories({ repository: applicationId, buildId });
try {
if (queueBuild.status === 'running') {
await saveBuildLog({
line: 'Building halted, restarting...',
buildId,
applicationId: application.id
});
}
const volumes =
persistentStorage?.map((storage) => {
if (storage.oldPath) {
return `${applicationId}${storage.path
.replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`;
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
if (destinationDockerId) {
await prisma.build.update({
where: { id: buildId },
data: { status: 'running' }
});
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}`
});
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({
dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
}
}
}
} catch (error) {
//
}
let envs = [];
if (secrets.length > 0) {
envs = [
...envs,
...generateSecrets(secrets, pullmergeRequestId, false, port)
];
}
await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
if (dockerRegistry) {
const { url, username, password } = dockerRegistry;
await saveDockerRegistryCredentials({ url, username, password, workdir });
}
const labels = makeLabelForSimpleDockerfile({
applicationId,
type,
port: exposePort ? `${exposePort}:${port}` : port
});
try {
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[applicationId]: {
build: {
context: workdir
},
image: `${applicationId}:${buildId}`,
container_name: applicationId,
volumes,
labels,
environment: envs,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network)
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeCommand({
debug: true,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
}
} catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
if (error !== 1) {
await saveBuildLog({ line: error, buildId, applicationId: application.id });
}
if (error instanceof Error) {
await saveBuildLog({
line: error.message,
buildId,
applicationId: application.id
});
}
await fs.rm(workdir, { recursive: true, force: true });
return;
}
try {
if (application.dockerRegistryImageName) {
const customTag = application.dockerRegistryImageName.split(':')[1] || buildId;
const imageName = application.dockerRegistryImageName.split(':')[0];
await saveBuildLog({
line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
buildId,
applicationId: application.id
});
await pushToRegistry(application, workdir, buildId, imageName, customTag);
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
}
} catch (error) {
if (error.stdout) {
await saveBuildLog({ line: error.stdout, buildId, applicationId });
}
if (error.stderr) {
await saveBuildLog({ line: error.stderr, buildId, applicationId });
}
} finally {
await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({
where: { id: buildId },
data: { status: 'success' }
});
}
return;
}
const originalApplicationId = application.id;
const {
id: applicationId,
name,
destinationDocker,
destinationDockerId,
gitSource,
configHash,
fqdn,
projectId,
secrets,
phpModules,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage,
deploymentType,
gitCommitHash,
dockerRegistry
} = application;
let {
branch,
repository,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
dockerComposeFileLocation,
dockerComposeConfiguration,
denoMainFile
} = application;
let imageId = applicationId;
let domain = getDomain(fqdn);
let location = null;
let tag = null;
let customTag = null;
let imageName = null;
let imageFoundLocally = false;
let imageFoundRemotely = false;
if (pullmergeRequestId) {
const previewApplications = await prisma.previewApplication.findMany({
where: { applicationId: originalApplicationId, pullmergeRequestId }
});
if (previewApplications.length > 0) {
previewApplicationId = previewApplications[0].id;
}
// Previews, we need to get the source branch and set subdomain
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
repository = sourceRepository || repository;
}
const { workdir, repodir } = await createDirectories({ repository, buildId });
try {
if (queueBuild.status === 'running') {
await saveBuildLog({
line: 'Building halted, restarting...',
buildId,
applicationId: application.id
});
}
const currentHash = crypto
.createHash('sha256')
.update(
JSON.stringify({
pythonWSGI,
pythonModule,
pythonVariable,
deploymentType,
denoOptions,
baseImage,
baseBuildImage,
buildPack,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
secrets,
branch,
repository,
fqdn
})
)
.digest('hex');
const { debug } = settings;
if (!debug) {
await saveBuildLog({
line: `Debug logging is disabled. Enable it above if necessary!`,
buildId,
applicationId
});
}
const volumes =
persistentStorage?.map((storage) => {
if (storage.oldPath) {
return `${applicationId}${storage.path
.replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`;
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
} catch (error) {}
let deployNeeded = true;
let destinationType;
if (destinationDockerId) {
destinationType = 'docker';
}
if (destinationType === 'docker') {
await prisma.build.update({
where: { id: buildId },
data: { status: 'running' }
});
const configuration = await setDefaultConfiguration(application);
buildPack = configuration.buildPack;
port = configuration.port;
installCommand = configuration.installCommand;
startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation;
dockerComposeFileLocation = configuration.dockerComposeFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId,
debug,
workdir,
repodir,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort,
gitCommitHash,
configuration,
repository,
branch,
buildId,
apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null,
forPublic: gitSource.forPublic
});
if (!commit) {
throw new Error('No commit found?');
}
tag = commit.slice(0, 7);
if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
}
if (application.dockerRegistryImageName) {
imageName = application.dockerRegistryImageName.split(':')[0];
customTag = application.dockerRegistryImageName.split(':')[1] || tag;
} else {
customTag = tag;
imageName = applicationId;
}
if (pullmergeRequestId) {
customTag = `${customTag}-${pullmergeRequestId}`;
}
try {
await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {}
if (!pullmergeRequestId) {
if (configHash !== currentHash) {
deployNeeded = true;
if (configHash) {
await saveBuildLog({
line: 'Configuration changed',
buildId,
applicationId
});
}
} else {
deployNeeded = false;
}
} else {
deployNeeded = true;
}
try {
await executeCommand({
dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}`
});
imageFoundLocally = true;
} catch (error) {
//
}
if (dockerRegistry) {
const { url, username, password } = dockerRegistry;
location = await saveDockerRegistryCredentials({
url,
username,
password,
workdir
});
}
try {
await executeCommand({
dockerId: destinationDocker.id,
command: `docker ${
location ? `--config ${location}` : ''
} pull ${imageName}:${customTag}`
});
imageFoundRemotely = true;
} catch (error) {
//
}
let imageFound = `${applicationId}:${tag}`;
if (imageFoundRemotely) {
imageFound = `${imageName}:${customTag}`;
}
await copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
);
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
if (forceRebuild) deployNeeded = true;
if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) {
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId,
applicationId,
domain,
name,
type,
volumes,
labels,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
publishDirectory,
debug,
commit,
tag,
workdir,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
phpModules,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
dockerComposeConfiguration,
dockerComposeFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
deploymentType,
forceRebuild
});
else {
await saveBuildLog({
line: `Build pack ${buildPack} not found`,
buildId,
applicationId
});
throw new Error(`Build pack ${buildPack} not found.`);
}
} else {
if (imageFoundRemotely || deployNeeded) {
await saveBuildLog({
line: `Container image ${imageFound} found in Docker Registry - reuising it`,
buildId,
applicationId
});
} else {
if (imageFoundLocally || deployNeeded) {
await saveBuildLog({
line: `Container image ${imageFound} found locally - reuising it`,
buildId,
applicationId
});
}
}
}
if (buildPack === 'compose') {
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}`
});
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({
dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
}
}
}
} catch (error) {
//
}
try {
await executeCommand({
debug,
buildId,
applicationId,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
await prisma.build.update({
where: { id: buildId },
data: { status: 'success' }
});
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
} else {
try {
const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${
pullmergeRequestId ? imageId : applicationId
}' --format {{.ID}}`
});
if (containers) {
const containerArray = containers.split('\n');
if (containerArray.length > 0) {
for (const container of containerArray) {
await executeCommand({
dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
}
}
}
} catch (error) {
//
}
let envs = [];
if (secrets.length > 0) {
envs = [
...envs,
...generateSecrets(secrets, pullmergeRequestId, false, port)
];
}
if (dockerRegistry) {
const { url, username, password } = dockerRegistry;
await saveDockerRegistryCredentials({ url, username, password, workdir });
}
try {
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: imageFound,
container_name: imageId,
volumes,
environment: envs,
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network)
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeCommand({
debug,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
if (!pullmergeRequestId)
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
}
}
} catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
if (error !== 1) {
await saveBuildLog({ line: error, buildId, applicationId: application.id });
}
if (error instanceof Error) {
await saveBuildLog({
line: error.message,
buildId,
applicationId: application.id
});
}
await fs.rm(workdir, { recursive: true, force: true });
return;
}
try {
if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) {
await saveBuildLog({
line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
buildId,
applicationId: application.id
});
await pushToRegistry(application, workdir, tag, imageName, customTag);
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
}
} catch (error) {
if (error.stdout) {
await saveBuildLog({ line: error.stdout, buildId, applicationId });
}
if (error.stderr) {
await saveBuildLog({ line: error.stderr, buildId, applicationId });
}
} finally {
await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
}
});
}
await pAll.default(actions, { concurrency });
}
} catch (error) {
console.log(error);
}
});
while (true) {
await th();
}
} else process.exit(0);
})();

View File

@ -0,0 +1,9 @@
import { parentPort } from 'node:worker_threads';
import process from 'node:process';
console.log('Hello TypeScript!');
// signal to parent that the job is done
if (parentPort) parentPort.postMessage('done');
// eslint-disable-next-line unicorn/no-process-exit
else process.exit(0);

View File

@ -9,6 +9,7 @@ import type { Config } from 'unique-names-generator';
import { env } from '../env';
import { day } from './dayjs';
import { executeCommand } from './executeCommand';
import { saveBuildLog } from './logging';
const customConfig: Config = {
dictionaries: [adjectives, colors, animals],
@ -526,3 +527,11 @@ export const scanningTemplates = {
buildPack: 'react'
}
};
export async function cleanupDB(buildId: string, applicationId: string) {
const data = await prisma.build.findUnique({ where: { id: buildId } });
if (data?.status === 'queued' || data?.status === 'running') {
await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } });
}
await saveBuildLog({ line: 'Canceled.', buildId, applicationId });
}

View File

@ -0,0 +1,26 @@
import Bree from 'bree';
import path from 'path';
import Cabin from 'cabin';
import TSBree from '@breejs/ts-worker';
export const isDev = process.env['NODE_ENV'] === 'development';
Bree.extend(TSBree);
const options: any = {
defaultExtension: 'js',
logger: new Cabin(),
// logger: false,
// workerMessageHandler: async ({ name, message }) => {
// if (name === 'deployApplication' && message?.deploying) {
// if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
// scheduler.workers.get('deployApplication').postMessage('cancel')
// }
// }
// },
// jobs: [{ name: 'deployApplication' }]
jobs: [{ name: 'worker' }]
};
if (isDev) options.root = path.join(__dirname, '../jobs');
export const scheduler = new Bree(options);

View File

@ -7,6 +7,8 @@ import * as path from 'node:path';
import serve from '@fastify/static';
import autoLoad from '@fastify/autoload';
// import { prisma } from './prisma';
import Graceful from '@ladjs/graceful';
import { scheduler } from './scheduler';
const isDev = process.env['NODE_ENV'] === 'development';
@ -60,6 +62,9 @@ export function createServer(opts: ServerOptions) {
try {
await server.listen({ host: '0.0.0.0', port });
console.log('Coolify server is listening on port', port, 'at 0.0.0.0 🚀');
const graceful = new Graceful({ brees: [scheduler] });
graceful.listen();
scheduler.run('worker');
} catch (err) {
server.log.error(err);
process.exit(1);

View File

@ -20,6 +20,7 @@ import cuid from 'cuid';
import {
checkDomainsIsValidInDNS,
checkExposedPort,
cleanupDB,
createDirectories,
decrypt,
encrypt,
@ -29,8 +30,220 @@ import {
saveDockerRegistryCredentials,
setDefaultConfiguration
} from '../../../lib/common';
import { day } from '../../../lib/dayjs';
import csv from 'csvtojson';
export const applicationsRouter = router({
resetQueue: privateProcedure.mutation(async ({ ctx }) => {
const teamId = ctx.user.teamId;
if (teamId === '0') {
await prisma.build.updateMany({
where: { status: { in: ['queued', 'running'] } },
data: { status: 'canceled' }
});
// scheduler.workers.get("deployApplication").postMessage("cancel");
}
}),
cancelBuild: privateProcedure
.input(
z.object({
buildId: z.string(),
applicationId: z.string()
})
)
.mutation(async ({ input }) => {
const { buildId, applicationId } = input;
let count = 0;
await new Promise<void>(async (resolve, reject) => {
const { destinationDockerId, status } = await prisma.build.findFirst({
where: { id: buildId }
});
const { id: dockerId } = await prisma.destinationDocker.findFirst({
where: { id: destinationDockerId }
});
const interval = setInterval(async () => {
try {
if (status === 'failed' || status === 'canceled') {
clearInterval(interval);
return resolve();
}
if (count > 15) {
clearInterval(interval);
// if (scheduler.workers.has('deployApplication')) {
// scheduler.workers.get('deployApplication').postMessage('cancel');
// }
await cleanupDB(buildId, applicationId);
return reject(new Error('Canceled.'));
}
const { stdout: buildContainers } = await executeCommand({
dockerId,
command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
});
if (buildContainers) {
const containersArray = buildContainers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
if (!containerObj.Names.startsWith(`${applicationId} `)) {
await removeContainer({ id, dockerId });
clearInterval(interval);
// if (scheduler.workers.has('deployApplication')) {
// scheduler.workers.get('deployApplication').postMessage('cancel');
// }
await cleanupDB(buildId, applicationId);
return resolve();
}
}
}
count++;
} catch (error) {}
}, 100);
});
}),
getBuildLogs: privateProcedure
.input(
z.object({
id: z.string(),
buildId: z.string(),
sequence: z.number()
})
)
.query(async ({ input }) => {
let { id, buildId, sequence } = input;
let file = `/app/logs/${id}_buildlog_${buildId}.csv`;
if (isDev) {
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`;
}
const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc();
try {
await fs.stat(file);
} catch (error) {
let logs = await prisma.buildLog.findMany({
where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' }
});
const data = await prisma.build.findFirst({ where: { id: buildId } });
const createdAt = day(data.createdAt).utc();
return {
logs: logs.map((log) => {
log.time = Number(log.time);
return log;
}),
fromDb: true,
took: day().diff(createdAt) / 1000,
status: data?.status || 'queued'
};
}
let fileLogs = (await fs.readFile(file)).toString();
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs);
let logs = decryptedLogs
.map((log) => {
const parsed = {
time: log['field1'],
line: decrypt(log['field2'] + '","' + log['field3'])
};
return parsed;
})
.filter((log) => log.time > sequence);
return {
logs,
fromDb: false,
took: day().diff(createdAt) / 1000,
status: data?.status || 'queued'
};
}),
getBuilds: privateProcedure
.input(
z.object({
id: z.string(),
buildId: z.string().optional(),
skip: z.number()
})
)
.query(async ({ input }) => {
let { id, buildId, skip } = input;
let builds = [];
const buildCount = await prisma.build.count({ where: { applicationId: id } });
if (buildId) {
builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } });
} else {
builds = await prisma.build.findMany({
where: { applicationId: id },
orderBy: { createdAt: 'desc' },
take: 5 + skip
});
}
builds = builds.map((build) => {
if (build.status === 'running') {
build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
}
return build;
});
return {
builds,
buildCount
};
}),
loadLogs: privateProcedure
.input(
z.object({
id: z.string(),
containerId: z.string(),
since: z.number()
})
)
.query(async ({ input }) => {
let { id, containerId, since } = input;
if (since !== 0) {
since = day(since).unix();
}
const {
destinationDockerId,
destinationDocker: { id: dockerId }
} = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
try {
const { default: ansi } = await import('strip-ansi');
const { stdout, stderr } = await executeCommand({
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);
const sortedLogs = logs.sort((a, b) =>
day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1
);
return { logs: sortedLogs };
// }
} catch (error) {
const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true };
}
if (statusCode === 404) {
return {
logs: []
};
}
}
}
return {
message: 'No logs found.'
};
}),
getStorages: privateProcedure
.input(
z.object({

View File

@ -166,6 +166,7 @@ importers:
autoprefixer: 10.4.13
cuid: 2.1.8
daisyui: 2.41.0
dayjs: 1.11.6
eslint: 8.28.0
eslint-config-prettier: 8.5.0
eslint-plugin-svelte3: 4.0.0
@ -192,6 +193,7 @@ importers:
'@trpc/server': 10.1.0
cuid: 2.1.8
daisyui: 2.41.0_2lwn2upnx27dqeg6hqdu7sq75m
dayjs: 1.11.6
flowbite-svelte: 0.28.0
js-cookie: 3.0.1
js-yaml: 4.1.0
@ -236,12 +238,14 @@ importers:
apps/server:
specifiers:
'@breejs/ts-worker': 2.0.0
'@fastify/autoload': 5.6.0
'@fastify/cors': 8.2.0
'@fastify/env': 4.1.0
'@fastify/jwt': 6.5.0
'@fastify/static': 6.6.0
'@fastify/websocket': 7.1.1
'@ladjs/graceful': 3.0.2
'@prisma/client': 4.6.1
'@trpc/client': 10.1.0
'@trpc/server': 10.1.0
@ -253,7 +257,11 @@ importers:
'@types/shell-quote': ^1.7.1
'@types/ws': 8.5.3
abort-controller: 3.0.0
axe: 11.0.0
bcryptjs: 2.4.3
bree: 9.1.2
cabin: 11.0.1
csvtojson: 2.0.10
cuid: 2.1.8
dayjs: 1.11.6
dotenv: ^16.0.3
@ -267,11 +275,14 @@ importers:
jsonwebtoken: 8.5.1
node-fetch: 3.3.0
npm-run-all: 4.1.5
p-all: 4.0.0
p-throttle: 5.0.0
prisma: 4.6.1
rimraf: 3.0.2
shell-quote: ^1.7.4
ssh-config: 4.1.6
start-server-and-test: 1.14.0
strip-ansi: 7.0.1
superjson: 1.11.0
tslib: 2.4.1
tsx: 3.12.1
@ -281,17 +292,23 @@ importers:
ws: 8.11.0
zod: 3.19.1
dependencies:
'@breejs/ts-worker': 2.0.0_7ja7ufy2vbczkqoi6dab6h7sdi
'@fastify/autoload': 5.6.0
'@fastify/cors': 8.2.0
'@fastify/env': 4.1.0
'@fastify/jwt': 6.5.0
'@fastify/static': 6.6.0
'@fastify/websocket': 7.1.1
'@ladjs/graceful': 3.0.2
'@prisma/client': 4.6.1_prisma@4.6.1
'@trpc/client': 10.1.0_@trpc+server@10.1.0
'@trpc/server': 10.1.0
abort-controller: 3.0.0
axe: 11.0.0
bcryptjs: 2.4.3
bree: 9.1.2
cabin: 11.0.1_axe@11.0.0
csvtojson: 2.0.10
cuid: 2.1.8
dayjs: 1.11.6
dotenv: 16.0.3
@ -304,9 +321,12 @@ importers:
js-yaml: 4.1.0
jsonwebtoken: 8.5.1
node-fetch: 3.3.0
p-all: 4.0.0
p-throttle: 5.0.0
prisma: 4.6.1
shell-quote: 1.7.4
ssh-config: 4.1.6
strip-ansi: 7.0.1
superjson: 1.11.0
tslib: 2.4.1
unique-names-generator: 4.7.1
@ -1424,6 +1444,22 @@ packages:
engines: {node: '>= 10'}
dev: false
/@breejs/ts-worker/2.0.0_7ja7ufy2vbczkqoi6dab6h7sdi:
resolution: {integrity: sha512-6anHRcmgYlF7mrm/YVRn6rx2cegLuiY3VBxkkimOTWC/dVQeH336imVSuIKEGKTwiuNTPr2hswVdDSneNuXg3A==}
engines: {node: '>= 12.11'}
peerDependencies:
bree: '>=9.0.0'
dependencies:
bree: 9.1.2
ts-node: 10.8.2_wup25etrarvlqkprac7h35hj7u
tsconfig-paths: 4.1.0
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
- '@types/node'
- typescript
dev: false
/@breejs/ts-worker/2.0.0_rfg2b5n3b6pycmpydtv43bmupy:
resolution: {integrity: sha512-6anHRcmgYlF7mrm/YVRn6rx2cegLuiY3VBxkkimOTWC/dVQeH336imVSuIKEGKTwiuNTPr2hswVdDSneNuXg3A==}
engines: {node: '>= 12.11'}