diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 25e97c31a..eda081b6c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,7 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [3000, 3001], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "cp apps/api/.env.example pps/api/.env && pnpm install && pnpm db:push && pnpm db:seed", + "postCreateCommand": "cp apps/api/.env.example apps/api/.env && pnpm install && pnpm db:push && pnpm db:seed", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node", "features": { diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index fbcb07616..178a06aca 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -21,14 +21,17 @@ async function autoUpdater() { const activeCount = 0 if (activeCount === 0) { if (!isDev) { - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` - ); + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + if (isAutoUpdateEnabled) { + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + ); + await asyncExecShell( + `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` + ); + } } else { console.log('Updating (not really in dev mode).'); } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 7f271d900..be203edba 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -205,7 +205,7 @@ export async function isDNSValid(hostname: any, domain: string): Promise { const { isIP } = await import('is-ip'); const { DNSServers } = await listSettings(); if (DNSServers) { - dns.setServers([DNSServers]); + dns.setServers([...DNSServers.split(',')]); } let resolves = []; try { @@ -316,7 +316,7 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P const { DNSServers } = await listSettings(); if (DNSServers) { - dns.setServers([DNSServers]); + dns.setServers([...DNSServers.split(',')]); } let resolves = []; @@ -547,22 +547,26 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)); } -export async function executeDockerCmd({ - debug, - buildId, - applicationId, - dockerId, - command -}: { - debug?: boolean; - buildId?: string; - applicationId?: string; - dockerId: string; - command: string; -}): Promise { - let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ - where: { id: dockerId } - }); +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, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId); engine = `ssh://${remoteIpAddress}`; @@ -577,7 +581,7 @@ export async function executeDockerCmd({ if (command.startsWith(`docker build --progress plain`)) { return await asyncExecShellStream({ debug, buildId, applicationId, command, engine }); } - return await asyncExecShell(`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`); + return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true }) } export async function startTraefikProxy(id: string): Promise { const { engine, network, remoteEngine, remoteIpAddress } = @@ -822,7 +826,6 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data defaultDatabase, version, type, - settings: { appendOnly } } = database; const baseImage = getDatabaseImage(type, arch); if (type === 'mysql') { @@ -903,6 +906,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data } return configuration; } else if (type === 'redis') { + const { settings: { appendOnly } } = database; const configuration: DatabaseConfiguration = { privatePort: 6379, command: undefined, @@ -1182,113 +1186,150 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } } } -export async function checkExposedPort({ - id, - configuredPort, - exposePort, - dockerId, - remoteIpAddress -}: { - id: string; - configuredPort?: number; - exposePort: number; - dockerId: string; - remoteIpAddress?: string; -}) { +export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) { if (exposePort < 1024 || exposePort > 65535) { throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }; } if (configuredPort) { if (configuredPort !== exposePort) { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` }; } } } else { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` }; } } } -export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) { +export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) { const { default: checkPort } = await import('is-port-reachable'); - const applicationUsed = await ( - await prisma.application.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { exposePort: true } - }) - ).map((a) => a.exposePort); - const serviceUsed = await ( - await prisma.service.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { exposePort: true } - }) - ).map((a) => a.exposePort); - const usedPorts = [...applicationUsed, ...serviceUsed]; - if (usedPorts.includes(exposePort)) { - return false; + if (remoteEngine) { + const applicationUsed = await ( + await prisma.application.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const serviceUsed = await ( + await prisma.service.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const usedPorts = [...applicationUsed, ...serviceUsed]; + if (usedPorts.includes(exposePort)) { + return false + } + const found = await checkPort(exposePort, { host: remoteIpAddress }); + if (!found) { + return exposePort + } + return false + } else { + const applicationUsed = await ( + await prisma.application.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const serviceUsed = await ( + await prisma.service.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const usedPorts = [...applicationUsed, ...serviceUsed]; + if (usedPorts.includes(exposePort)) { + return false + } + const found = await checkPort(exposePort, { host: 'localhost' }); + if (!found) { + return exposePort + } + return false } - const found = await checkPort(exposePort, { host: remoteIpAddress || 'localhost' }); - if (!found) { - return exposePort; - } - return false; } export function generateRangeArray(start, end) { return Array.from({ length: end - start }, (v, k) => k + start); } -export async function getFreePublicPort(id, dockerId) { +export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) { const { default: isReachable } = await import('is-port-reachable'); const data = await prisma.setting.findFirst(); const { minPort, maxPort } = data; - const dbUsed = await ( - await prisma.database.findMany({ - where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { publicPort: true } - }) - ).map((a) => a.publicPort); - const wpFtpUsed = await ( - await prisma.wordpress.findMany({ - where: { - ftpPublicPort: { not: null }, - id: { not: id }, - service: { destinationDockerId: dockerId } - }, - select: { ftpPublicPort: true } - }) - ).map((a) => a.ftpPublicPort); - const wpUsed = await ( - await prisma.wordpress.findMany({ - where: { - mysqlPublicPort: { not: null }, - id: { not: id }, - service: { destinationDockerId: dockerId } - }, - select: { mysqlPublicPort: true } - }) - ).map((a) => a.mysqlPublicPort); - const minioUsed = await ( - await prisma.minio.findMany({ - where: { - publicPort: { not: null }, - id: { not: id }, - service: { destinationDockerId: dockerId } - }, - select: { publicPort: true } - }) - ).map((a) => a.publicPort); - const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; - const range = generateRangeArray(minPort, maxPort); - const availablePorts = range.filter((port) => !usedPorts.includes(port)); - for (const port of availablePorts) { - const found = await isReachable(port, { host: 'localhost' }); - if (!found) { - return port; + if (remoteEngine) { + const dbUsed = await ( + await prisma.database.findMany({ + where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const wpFtpUsed = await ( + await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { ftpPublicPort: true } + }) + ).map((a) => a.ftpPublicPort); + const wpUsed = await ( + await prisma.wordpress.findMany({ + where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { mysqlPublicPort: true } + }) + ).map((a) => a.mysqlPublicPort); + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; + const range = generateRangeArray(minPort, maxPort) + const availablePorts = range.filter(port => !usedPorts.includes(port)) + for (const port of availablePorts) { + const found = await isReachable(port, { host: remoteIpAddress }) + if (!found) { + return port + } } + return false + } else { + const dbUsed = await ( + await prisma.database.findMany({ + where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const wpFtpUsed = await ( + await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { ftpPublicPort: true } + }) + ).map((a) => a.ftpPublicPort); + const wpUsed = await ( + await prisma.wordpress.findMany({ + where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { mysqlPublicPort: true } + }) + ).map((a) => a.mysqlPublicPort); + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; + const range = generateRangeArray(minPort, maxPort) + const availablePorts = range.filter(port => !usedPorts.includes(port)) + for (const port of availablePorts) { + const found = await isReachable(port, { host: 'localhost' }) + if (!found) { + return port + } + } + return false } - return false; } export async function startTraefikTCPProxy( diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index f56657ce7..7f13f7219 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -321,8 +321,8 @@ async function startMinioService(request: FastifyRequest) { const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('minio'); - const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) - const publicPort = await getFreePublicPort(id, dockerId); + const { service: { destinationDocker: { remoteEngine, engine, remoteIpAddress } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) + const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }); const consolePort = 9001; const { workdir } = await createDirectories({ repository: type, buildId: id }); @@ -1979,8 +1979,8 @@ async function startGlitchTipService(request: FastifyRequest) EMAIL_PORT: emailSmtpPort, EMAIL_HOST_USER: emailSmtpUser, EMAIL_HOST_PASSWORD: emailSmtpPassword, - EMAIL_USE_TLS: emailSmtpUseTls, - EMAIL_USE_SSL: emailSmtpUseSsl, + EMAIL_USE_TLS: emailSmtpUseTls ? 'True' : 'False', + EMAIL_USE_SSL: emailSmtpUseSsl ? 'True' : 'False', EMAIL_BACKEND: emailBackend, MAILGUN_API_KEY: mailgunApiKey, SENDGRID_API_KEY: sendgridApiKey, diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 1d00e94d1..8a390a3f5 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -252,8 +252,8 @@ export async function saveApplication(request: FastifyRequest, exposePort = Number(exposePort); } - const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (denoOptions) denoOptions = denoOptions.trim(); const defaultConfiguration = await setDefaultConfiguration({ buildPack, @@ -534,14 +534,14 @@ export async function checkDNS(request: FastifyRequest) { } if (exposePort) exposePort = Number(exposePort); - const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) + const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); const found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 5e78dbfe9..16d43205c 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -6,8 +6,8 @@ import fs from 'fs/promises'; import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; -import { DeleteDatabaseSecret, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from '../../../../types'; -import { DeleteDatabase, SaveDatabaseType } from './types'; +import type { OnlyId } from '../../../../types'; +import type { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveDatabaseType, SaveVersion } from './types'; export async function listDatabases(request: FastifyRequest) { try { @@ -94,15 +94,14 @@ export async function getDatabase(request: FastifyRequest) { if (!database) { throw { status: 404, message: 'Database not found.' } } - const { arch } = await listSettings(); + const settings = await listSettings(); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); - const configuration = generateDatabaseConfiguration(database, arch); - const settings = await listSettings(); + const configuration = generateDatabaseConfiguration(database, settings.arch); return { privatePort: configuration?.privatePort, database, - versions: await getDatabaseVersions(database.type, arch), + versions: await getDatabaseVersions(database.type, settings.arch), settings }; } catch ({ status, message }) { @@ -426,10 +425,10 @@ export async function saveDatabaseSettings(request: FastifyRequest { 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..c917836d2 --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -0,0 +1,119 @@ +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 servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] }) + // const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] }) + + return { + servers + } + } 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/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index c6858a7d7..c6f10cbb4 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -1,7 +1,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited } from '../../../../lib/docker'; import cuid from 'cuid'; @@ -70,6 +70,7 @@ export async function getService(request: FastifyRequest) { throw { status: 404, message: 'Service not found.' } } return { + settings: await listSettings(), service } } catch ({ status, message }) { @@ -232,7 +233,7 @@ export async function checkService(request: FastifyRequest) { if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (exposePort) exposePort = Number(exposePort); - const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) + const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); let found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); @@ -247,7 +248,7 @@ export async function checkService(request: FastifyRequest) { } } } - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; @@ -484,9 +485,9 @@ export async function activateWordpressFtp(request: FastifyRequest, reply: const { fqdn } = request.body const { DNSServers } = await listSettings(); if (DNSServers) { - dns.setServers([DNSServers]); + dns.setServers([...DNSServers.split(',')]); } let ip; try { diff --git a/apps/ui/src/lib/components/UpdateAvailable.svelte b/apps/ui/src/lib/components/UpdateAvailable.svelte index db98d8699..55f5bc8e3 100644 --- a/apps/ui/src/lib/components/UpdateAvailable.svelte +++ b/apps/ui/src/lib/components/UpdateAvailable.svelte @@ -4,6 +4,7 @@ import { addToast, appSession, features } from '$lib/store'; import { asyncSleep, errorNotification } from '$lib/common'; import { onMount } from 'svelte'; + import Tooltip from './Tooltip.svelte'; let isUpdateAvailable = false; let updateStatus: any = { @@ -75,14 +76,14 @@ }); -
+
{#if $appSession.teamId === '0'} {#if isUpdateAvailable} + New Version Available! {/if} {/if}
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/lib/store.ts b/apps/ui/src/lib/store.ts index 5355d22c8..ec841a2e5 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -26,6 +26,8 @@ interface AddToast { message: string, timeout?: number | undefined } + +export const search: any = writable('') export const loginEmail: Writable = writable() export const appSession: Writable = writable({ isRegistrationEnabled: false, @@ -84,7 +86,8 @@ export const status: Writable = writable({ isRunning: false, isExited: false, loading: false, - initialLoading: true + initialLoading: true, + isPublic: false } }); diff --git a/apps/ui/src/routes/_NewResource.svelte b/apps/ui/src/routes/_NewResource.svelte index ccc15e8d9..5729ae81c 100644 --- a/apps/ui/src/routes/_NewResource.svelte +++ b/apps/ui/src/routes/_NewResource.svelte @@ -38,13 +38,13 @@