wip: trpc
This commit is contained in:
parent
e3e39af6fb
commit
c8f7ca920e
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
};
|
7
apps/client/src/lib/dayjs.ts
Normal file
7
apps/client/src/lib/dayjs.ts
Normal 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 };
|
@ -170,3 +170,4 @@ export const setLocation = (resource: any, settings?: any) => {
|
||||
disabledButton.set(false);
|
||||
}
|
||||
};
|
||||
export const selectedBuildId: any = writable(null)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
204
apps/client/src/routes/applications/[id]/builds/+page.svelte
Normal file
204
apps/client/src/routes/applications/[id]/builds/+page.svelte
Normal 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>
|
16
apps/client/src/routes/applications/[id]/builds/+page.ts
Normal file
16
apps/client/src/routes/applications/[id]/builds/+page.ts
Normal 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.'
|
||||
});
|
||||
}
|
||||
};
|
215
apps/client/src/routes/applications/[id]/builds/BuildLog.svelte
Normal file
215
apps/client/src/routes/applications/[id]/builds/BuildLog.svelte
Normal 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}
|
@ -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"
|
176
apps/client/src/routes/applications/[id]/logs/+page.svelte
Normal file
176
apps/client/src/routes/applications/[id]/logs/+page.svelte
Normal 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}
|
@ -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';
|
||||
|
||||
|
@ -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",
|
||||
|
807
apps/server/src/jobs/deployApplication.ts
Normal file
807
apps/server/src/jobs/deployApplication.ts
Normal 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);
|
||||
})();
|
9
apps/server/src/jobs/worker.ts
Normal file
9
apps/server/src/jobs/worker.ts
Normal 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);
|
@ -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 });
|
||||
}
|
||||
|
26
apps/server/src/scheduler.ts
Normal file
26
apps/server/src/scheduler.ts
Normal 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);
|
@ -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);
|
||||
|
@ -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({
|
||||
|
@ -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'}
|
||||
|
Loading…
Reference in New Issue
Block a user