diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 4e843c900..50a8dd4db 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -499,9 +499,26 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)) } +export async function executeSSHCmd({ dockerId, command }) { + const { execaCommand } = await import('execa') + let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + if (remoteEngine) { + await createRemoteEngineConfiguration(dockerId) + engine = `ssh://${remoteIpAddress}` + } else { + engine = 'unix:///var/run/docker.sock' + } + if (process.env.CODESANDBOX_HOST) { + if (command.startsWith('docker compose')) { + command = command.replace(/docker compose/gi, 'docker-compose') + } + } + command = `ssh ${remoteIpAddress} ${command}` + return await execaCommand(command) +} export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise { const { execaCommand } = await import('execa') - let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId) engine = `ssh://${remoteIpAddress}` diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 09dfbea48..cdb115af3 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,5 +1,4 @@ -import os from 'node:os'; -import osu from 'node-os-utils'; + import axios from 'axios'; import { compareVersions } from 'compare-versions'; import cuid from 'cuid'; @@ -15,9 +14,10 @@ export async function hashPassword(password: string): Promise { return bcrypt.hash(password, saltRounds); } -export async function cleanupManually() { +export async function cleanupManually(request: FastifyRequest) { try { - const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } }) + const { serverId } = request.body; + const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) await cleanupDockerStorage(destination.id, true, true) return {} } catch ({ status, message }) { @@ -86,25 +86,7 @@ export async function restartCoolify(request: FastifyRequest) { return errorHandler({ status, message }) } } -export async function showUsage() { - try { - return { - usage: { - uptime: os.uptime(), - memory: await osu.mem.info(), - cpu: { - load: os.loadavg(), - usage: await osu.cpu.usage(), - count: os.cpus().length - }, - disk: await osu.drive.info('/') - } - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} export async function showDashboard(request: FastifyRequest) { try { const userId = request.user.userId; diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 8f9f821f8..52310998d 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -43,17 +43,13 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await showDashboard(request)); - fastify.get('/usage', { - onRequest: [fastify.authenticate] - }, async () => await showUsage()); - fastify.post('/internal/restart', { onRequest: [fastify.authenticate] }, async (request) => await restartCoolify(request)); fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] - }, async () => await cleanupManually()); + }, async (request) => await cleanupManually(request)); }; export default root; diff --git a/apps/api/src/routes/api/v1/servers/handlers.ts b/apps/api/src/routes/api/v1/servers/handlers.ts new file mode 100644 index 000000000..9174d9bf0 --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -0,0 +1,117 @@ +import type { FastifyRequest } from 'fastify'; +import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common'; +import os from 'node:os'; +import osu from 'node-os-utils'; + + +export async function listServers(request: FastifyRequest) { + try { + const userId = request.user.userId; + const teamId = request.user.teamId; + const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] }) + return { + servers: remoteServers + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +const mappingTable = [ + ['K total memory', 'totalMemoryKB'], + ['K used memory', 'usedMemoryKB'], + ['K active memory', 'activeMemoryKB'], + ['K inactive memory', 'inactiveMemoryKB'], + ['K free memory', 'freeMemoryKB'], + ['K buffer memory', 'bufferMemoryKB'], + ['K swap cache', 'swapCacheKB'], + ['K total swap', 'totalSwapKB'], + ['K used swap', 'usedSwapKB'], + ['K free swap', 'freeSwapKB'], + ['non-nice user cpu ticks', 'nonNiceUserCpuTicks'], + ['nice user cpu ticks', 'niceUserCpuTicks'], + ['system cpu ticks', 'systemCpuTicks'], + ['idle cpu ticks', 'idleCpuTicks'], + ['IO-wait cpu ticks', 'ioWaitCpuTicks'], + ['IRQ cpu ticks', 'irqCpuTicks'], + ['softirq cpu ticks', 'softIrqCpuTicks'], + ['stolen cpu ticks', 'stolenCpuTicks'], + ['pages paged in', 'pagesPagedIn'], + ['pages paged out', 'pagesPagedOut'], + ['pages swapped in', 'pagesSwappedIn'], + ['pages swapped out', 'pagesSwappedOut'], + ['interrupts', 'interrupts'], + ['CPU context switches', 'cpuContextSwitches'], + ['boot time', 'bootTime'], + ['forks', 'forks'] +]; +function parseFromText(text) { + var data = {}; + var lines = text.split(/\r?\n/); + for (const line of lines) { + for (const [key, value] of mappingTable) { + if (line.indexOf(key) >= 0) { + const values = line.match(/[0-9]+/)[0]; + data[value] = parseInt(values, 10); + } + } + } + return data; +} +export async function showUsage(request: FastifyRequest) { + const { id } = request.params; + let { remoteEngine } = request.query + remoteEngine = remoteEngine === 'true' ? true : false + if (remoteEngine) { + const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` }) + const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) + const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` }) + // const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) + // console.log(cpuUsage) + const parsed: any = parseFromText(stats) + return { + usage: { + uptime: parsed.bootTime / 1024, + memory: { + totalMemMb: parsed.totalMemoryKB / 1024, + usedMemMb: parsed.usedMemoryKB / 1024, + freeMemMb: parsed.freeMemoryKB / 1024, + usedMemPercentage: (parsed.usedMemoryKB / parsed.totalMemoryKB) * 100, + freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100 + }, + cpu: { + load: 0, + usage: 0, + count: cpus + }, + disk: { + totalGb: (disks.split(' ')[0] / 1024).toFixed(1), + usedGb: (disks.split(' ')[1] / 1024).toFixed(1), + freeGb: (disks.split(' ')[0] - disks.split(' ')[1]).toFixed(1), + usedPercentage: disks.split(' ')[2].replace('%', ''), + freePercentage: 100 - disks.split(' ')[2].replace('%', '') + } + + } + } + } else { + try { + return { + usage: { + uptime: os.uptime(), + memory: await osu.mem.info(), + cpu: { + load: os.loadavg(), + usage: await osu.cpu.usage(), + count: os.cpus().length + }, + disk: await osu.drive.info('/') + } + + }; + } catch ({ status, message }) { + return errorHandler({ status, message }) + } + } + + +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/servers/index.ts b/apps/api/src/routes/api/v1/servers/index.ts new file mode 100644 index 000000000..7373d8cb4 --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/index.ts @@ -0,0 +1,14 @@ +import { FastifyPluginAsync } from 'fastify'; +import { listServers, showUsage } from './handlers'; + + +const root: FastifyPluginAsync = async (fastify): Promise => { + fastify.addHook('onRequest', async (request) => { + return await request.jwtVerify() + }) + fastify.get('/', async (request) => await listServers(request)); + fastify.get('/usage/:id', async (request) => await showUsage(request)); + +}; + +export default root; diff --git a/apps/api/src/routes/api/v1/servers/types.ts b/apps/api/src/routes/api/v1/servers/types.ts new file mode 100644 index 000000000..26a896b8e --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/types.ts @@ -0,0 +1,27 @@ +import { OnlyId } from "../../../../types" + +export interface SaveTeam extends OnlyId { + Body: { + name: string + } +} +export interface InviteToTeam { + Body: { + email: string, + permission: string, + teamId: string, + teamName: string + } +} +export interface BodyId { + Body: { + id: string + } +} +export interface SetPermission { + Body: { + userId: string, + newPermission: string, + permissionId: string + } +} \ No newline at end of file diff --git a/apps/ui/src/lib/components/Usage.svelte b/apps/ui/src/lib/components/Usage.svelte index a5128c6b6..7d4a82929 100644 --- a/apps/ui/src/lib/components/Usage.svelte +++ b/apps/ui/src/lib/components/Usage.svelte @@ -1,4 +1,5 @@ -
-
-

Hardware Details

-
- {#if $appSession.teamId === '0'} - - {/if} +
+ {#if loading.usage} + + {:else} + + {/if} + {#if server.remoteEngine} +
+ BETA
+ {/if} +
+
+

+ {server.name} +

+
+ {#if server?.remoteIpAddress} +

{server?.remoteIpAddress}

+ {:else} +

localhost

+ {/if} +
+
+ {#if $appSession.teamId === '0'} + + {/if} +
+
+
@@ -82,21 +109,21 @@
Total Memory
- {(usage?.memory.totalMemMb).toFixed(0)}MB + {(usage?.memory?.totalMemMb).toFixed(0)}MB
Used Memory
- {(usage?.memory.usedMemMb).toFixed(0)}MB + {(usage?.memory?.usedMemMb).toFixed(0)}MB
Free Memory
- {usage?.memory.freeMemPercentage}% + {(usage?.memory?.freeMemPercentage).toFixed(0)}%
@@ -105,41 +132,41 @@
Total CPU
- {usage?.cpu.count} + {usage?.cpu?.count}
CPU Usage
- {usage?.cpu.usage}% + {usage?.cpu?.usage}%
Load Average (5,10,30mins)
-
{usage?.cpu.load}
+
{usage?.cpu?.load}
Total Disk
- {usage?.disk.totalGb}GB + {usage?.disk?.totalGb}GB
Used Disk
- {usage?.disk.usedGb}GB + {usage?.disk?.usedGb}GB
Free Disk
- {usage?.disk.freePercentage}% + {usage?.disk?.freePercentage}%
diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index b9d346da5..e31d228ff 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -132,9 +132,9 @@ White labeled logo
{/if} -
+ Dashboard + Servers
diff --git a/apps/ui/src/routes/servers/index.svelte b/apps/ui/src/routes/servers/index.svelte new file mode 100644 index 000000000..d838e3410 --- /dev/null +++ b/apps/ui/src/routes/servers/index.svelte @@ -0,0 +1,47 @@ + + + + +
+
Servers
+
+
+ {#if servers.length > 0} +
+ {#each servers as server} +
+
+ {#if $appSession.teamId === '0'} + + {/if} +
+
+ {/each} +
+ {:else} +

Nothing here.

+ {/if} +