feat: remote docker engine

This commit is contained in:
Andras Bacsai 2022-07-21 12:43:53 +00:00
parent 4df32a9bc0
commit 890ea2e2d4
29 changed files with 598 additions and 503 deletions

1
TODO.md Normal file
View File

@ -0,0 +1 @@
- Database connecting string for remote docker engines

View File

@ -32,6 +32,7 @@
"dayjs": "1.11.3",
"dockerode": "3.3.2",
"dotenv-extended": "2.9.0",
"execa": "^6.1.0",
"fastify": "4.2.1",
"fastify-plugin": "4.0.0",
"generate-password": "1.7.0",

View File

@ -3,10 +3,8 @@ CREATE TABLE "SshKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"privateKey" TEXT NOT NULL,
"destinationDockerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "SshKey_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
"updatedAt" DATETIME NOT NULL
);
-- RedefineTables
@ -23,7 +21,9 @@ CREATE TABLE "new_DestinationDocker" (
"remoteVerified" BOOLEAN NOT NULL DEFAULT false,
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
"updatedAt" DATETIME NOT NULL,
"sshKeyId" TEXT,
CONSTRAINT "DestinationDocker_sshKeyId_fkey" FOREIGN KEY ("sshKeyId") REFERENCES "SshKey" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt" FROM "DestinationDocker";
DROP TABLE "DestinationDocker";
@ -31,6 +31,3 @@ ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
-- CreateIndex
CREATE UNIQUE INDEX "SshKey_destinationDockerId_key" ON "SshKey"("destinationDockerId");

View File

@ -213,17 +213,17 @@ model DestinationDocker {
updatedAt DateTime @updatedAt
database Database[]
service Service[]
sshKey SshKey?
sshKey SshKey? @relation(fields: [sshKeyId], references: [id])
sshKeyId String?
}
model SshKey {
id String @id @default(cuid())
name String
privateKey String
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
destinationDockerId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
name String
privateKey String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDocker DestinationDocker[]
}
model GitSource {

View File

@ -5,7 +5,7 @@ import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
import { createDirectories, decrypt, executeDockerCmd, getDomain, prisma } from '../lib/common';
import { dockerInstance, getEngine } from '../lib/docker';
import Dockerode from 'dockerode';
import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks';
@ -104,9 +104,6 @@ import * as buildpacks from '../lib/buildPacks';
destinationType = 'docker';
}
if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine);
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
const { workdir, repodir } = await createDirectories({ repository, buildId });
const configuration = await setDefaultConfiguration(message);
@ -185,18 +182,23 @@ import * as buildpacks from '../lib/buildPacks';
} else {
deployNeeded = true;
}
const image = await docker.engine.getImage(`${applicationId}:${tag}`);
let imageFound = false;
try {
await image.inspect();
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}`
})
imageFound = true;
} catch (error) {
//
}
if (!imageFound || deployNeeded) {
// if (!imageFound || deployNeeded) {
if (true) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
buildId,
applicationId,
domain,
@ -212,7 +214,6 @@ import * as buildpacks from '../lib/buildPacks';
commit,
tag,
workdir,
docker,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
@ -299,7 +300,7 @@ import * as buildpacks from '../lib/buildPacks';
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
networks: [destinationDocker.network],
labels,
depends_on: [],
restart: 'always',
@ -318,7 +319,7 @@ import * as buildpacks from '../lib/buildPacks';
}
},
networks: {
[docker.network]: {
[destinationDocker.network]: {
external: true
}
},

View File

@ -1,7 +1,8 @@
import { asyncExecShell, base64Encode, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { asyncExecShell, base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { scheduler } from "../scheduler";
import { promises as fs } from 'fs';
import { day } from "../dayjs";
import { spawn } from 'node:child_process'
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
@ -511,8 +512,8 @@ export async function buildImage({
applicationId,
tag,
workdir,
docker,
buildId,
dockerId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile'
@ -522,6 +523,9 @@ export async function buildImage({
} else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
}
if (debug) {
await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment. You will see the full build log when the build is finished!\n###############`, buildId, applicationId });
}
if (!debug && isCache) {
await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`,
@ -529,15 +533,56 @@ export async function buildImage({
applicationId
});
}
const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir },
{
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
const { stderr } = await executeDockerCmd({ dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
if (debug) {
const array = stderr.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
);
await streamEvents({ stream, docker, buildId, applicationId, debug });
}
// await new Promise((resolve, reject) => {
// const command = spawn(`docker`, ['build', '-f', `${workdir}${dockerFile}`, '-t', `${cache}`,`${workdir}`], {
// env: {
// DOCKER_HOST: 'ssh://root@95.217.178.202',
// DOCKER_BUILDKIT: '1'
// }
// });
// command.stdout.on('data', function (data) {
// console.log('stdout: ' + data);
// });
// command.stderr.on('data', function (data) {
// console.log('stderr: ' + data);
// });
// command.on('error', function (error) {
// console.log(error)
// reject(error)
// })
// command.on('exit', function (code) {
// console.log('exit code: ' + code);
// resolve(code)
// });
// })
// console.log({ stdout, stderr })
// const stream = await docker.engine.buildImage(
// { src: ['.'], context: workdir },
// {
// dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
// t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
// }
// );
// await streamEvents({ stream, docker, buildId, applicationId, debug });
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
}
@ -617,18 +662,17 @@ export function makeLabelForStandaloneApplication({
export async function buildCacheImageWithNode(data, imageForBuild) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets,
pullmergeRequestId
} = data;
data.isCache = true
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
@ -659,11 +703,13 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
await buildImage(data);
}
export async function buildCacheImageForLaravel(data, imageForBuild) {
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
const { workdir, buildId, secrets, pullmergeRequestId } = data;
data.isCache = true
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
@ -687,22 +733,17 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
await buildImage(data);
}
export async function buildCacheImageWithCargo(data, imageForBuild) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets
} = data;
data.isCache = true
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
@ -717,5 +758,5 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
await buildImage(data);
}

View File

@ -1,18 +1,18 @@
import { promises as fs } from 'fs';
import { buildImage } from './common';
export default async function ({
applicationId,
debug,
tag,
workdir,
docker,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
}) {
export default async function (data) {
let {
applicationId,
debug,
tag,
workdir,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
} = data
try {
const file = `${workdir}${dockerFileLocation}`;
let dockerFileOut = `${workdir}`;
@ -45,7 +45,7 @@ export default async function ({
}
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
await buildImage(data);
} catch (error) {
throw error;
}

View File

@ -473,6 +473,8 @@ export async function createRemoteEngineConfiguration(id: string) {
const sshKeyFile = `/tmp/id_rsa-${id}`
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
// Needed for remote docker compose
await asyncExecShell(`eval $(ssh-agent -s) && ssh-add -q ${sshKeyFile}`)
const config = sshConfig.parse('')
const found = config.find({ Host: remoteIpAddress })
if (!found) {
@ -542,14 +544,13 @@ export async function startTraefikProxy(id: string): Promise<void> {
data: { isCoolifyProxyUsed: true }
});
}
if (!remoteEngine) await configureNetworkTraefikProxy(engine);
if (!remoteEngine) await configureNetworkTraefikProxy(engine, id);
}
export async function configureNetworkTraefikProxy(engine: string): Promise<void> {
export async function configureNetworkTraefikProxy(engine: string, id: string): Promise<void> {
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const { stdout: networks } = await executeDockerCmd({
dockerId: '',
dockerId: id,
command:
`docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
});

View File

@ -47,13 +47,10 @@ export async function checkContainer({ dockerId, container, remove = false }: {
return containerFound;
}
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
let isExited = false;
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
if (stdout.trim() === 'exited') {
isExited = true;
}
@ -72,11 +69,11 @@ export async function removeContainer({
dockerId: string;
}): Promise<void> {
try {
const { stdout } =await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}`})
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}`})
await executeDockerCmd({ dockerId, command: `docker rm ${id}`})
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
console.log(error);

View File

@ -5,8 +5,8 @@ import axios from 'axios';
import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { asyncExecShell, checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine, isContainerExited, removeContainer } from '../../../../lib/docker';
import { asyncExecShell, checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, dockerInstance, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler';
import type { FastifyRequest } from 'fastify';
@ -62,23 +62,36 @@ export async function getImages(request: FastifyRequest<GetImages>) {
return errorHandler({ status, message })
}
}
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
isExited = await isContainerExited(application.destinationDocker.id, id);
}
return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplication(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
application,
appId
};
@ -284,8 +297,8 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const { id } = request.params
const { teamId } = request.user
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
const { engine, id: dockerId } = application.destinationDocker;
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
const found = await checkContainer({ dockerId, container: id });
if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id });
@ -304,11 +317,11 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
where: { id },
include: { destinationDocker: true }
});
if (application?.destinationDockerId && application.destinationDocker?.engine && application.destinationDocker?.network) {
const host = getEngine(application.destinationDocker.engine);
const { stdout: containers } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
);
if (application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
})
if (containers) {
const containersArray = containers.trim().split('\n');
for (const container of containersArray) {
@ -739,44 +752,39 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try {
const { id } = request.params
const { id } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
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 } = error;
if (statusCode === 404) {
return {
logs
logs: []
};
}
} catch (error) {
return {
logs: []
};
}
}
return {
message: 'No logs found.'
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers';
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage } from './types';
@ -17,6 +17,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveApplication>('/:id', async (request, reply) => await saveApplication(request, reply));
fastify.delete<DeleteApplication>('/:id', async (request, reply) => await deleteApplication(request, reply));
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<SaveApplicationSettings>('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply));

View File

@ -3,9 +3,10 @@ import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { asyncExecShell, ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { dockerInstance, getEngine } from '../../../../lib/docker';
import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker';
import { day } from '../../../../lib/dayjs';
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
import { SaveDatabaseType } from './types';
@ -98,7 +99,7 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
throw { status: 404, message: 'Database not found.' }
}
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const configuration = generateDatabaseConfiguration(database);
const settings = await listSettings();
return {
@ -236,7 +237,6 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
generateDatabaseConfiguration(database);
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const volumeName = volume.split(':')[0];
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
@ -322,39 +322,27 @@ export async function stopDatabase(request: FastifyRequest<OnlyId>) {
}
export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.database.findUnique({
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.database.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
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 } = error;
if (statusCode === 404) {

View File

@ -12,10 +12,11 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/new', async (request, reply) => await newDatabase(request, reply));
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));

View File

@ -2,7 +2,7 @@ import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import sshConfig from 'ssh-config'
import fs from 'fs/promises'
import { asyncExecShell, decrypt, errorHandler, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { asyncExecShell, decrypt, errorHandler, executeDockerCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types';
@ -67,9 +67,11 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
}
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
const teamId = request.user.teamId;
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
console.log({ name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort })
if (id === 'new') {
if (engine) {
const host = getEngine(engine);
@ -100,17 +102,12 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
}
}
return reply.code(201).send({ id: destination.id });
}
if (remoteIpAddress) {
await prisma.destinationDocker.create({
} else {
const destination = await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort }
});
return reply.code(201).send()
return reply.code(201).send({ id: destination.id })
}
throw {
message: `Cannot save Docker Engine.`
};
} else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
@ -125,22 +122,20 @@ export async function newDestination(request: FastifyRequest<NewDestination>, re
export async function deleteDestination(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine);
const { network } = destination;
const settings = await prisma.setting.findFirst();
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy';
const { stdout: found } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'`
);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
);
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } });
if (isCoolifyProxyUsed) {
if (engine || remoteVerified) {
const { stdout: found } = await executeDockerCmd({
dockerId: id,
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
})
if (found) {
await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` })
}
}
}
await prisma.destinationDocker.delete({ where: { id } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -168,6 +163,7 @@ export async function startProxy(request: FastifyRequest<Proxy>) {
await startTraefikProxy(id);
return {}
} catch ({ status, message }) {
console.log({status, message})
await stopTraefikProxy(id);
return errorHandler({ status, message })
}
@ -245,9 +241,9 @@ export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
const containerName = 'coolify-proxy';
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy' })
return {
isRunning: await checkContainer({ dockerId: destination.id, container: containerName })
isRunning
}
} catch ({ status, message }) {
return errorHandler({ status, message })

View File

@ -2,9 +2,9 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import bcrypt from 'bcryptjs';
import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions } from '../../../../lib/common';
import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, dockerInstance, getEngine, removeContainer } from '../../../../lib/docker';
import { checkContainer, dockerInstance, isContainerExited, removeContainer } from '../../../../lib/docker';
import cuid from 'cuid';
import type { OnlyId } from '../../../../types';
@ -172,6 +172,31 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
return errorHandler({ status, message })
}
}
export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
let isRunning = false;
let isExited = false
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service;
if (destinationDockerId) {
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
isExited = await isContainerExited(service.destinationDocker.id, id);
}
return {
isRunning,
isExited,
settings
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getService(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
@ -181,36 +206,8 @@ export async function getService(request: FastifyRequest<OnlyId>) {
if (!service) {
throw { status: 404, message: 'Service not found.' }
}
const { destinationDockerId, destinationDocker, type, version, settings } = service;
let isRunning = false;
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
const docker = dockerInstance({ destinationDocker });
const baseImage = getServiceImage(type);
const images = getServiceImages(type);
docker.engine.pull(`${baseImage}:${version}`);
if (images?.length > 0) {
for (const image of images) {
docker.engine.pull(`${image}:latest`);
}
}
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
return {
isRunning,
service,
settings
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -299,33 +296,22 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.service.findUnique({
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
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 } = error;
if (statusCode === 404) {
@ -742,7 +728,6 @@ async function startPlausibleAnalyticsService(request: FastifyRequest<ServiceSta
});
}
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('plausibleanalytics');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -859,10 +844,8 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -905,7 +888,6 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('nocodb');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -956,8 +938,8 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -970,7 +952,6 @@ async function stopNocodbService(request: FastifyRequest<ServiceStartStop>) {
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, dockerId: destinationDocker.id });
@ -999,7 +980,6 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('minio');
const publicPort = await getFreePort();
@ -1058,8 +1038,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
return {}
} catch ({ status, message }) {
@ -1071,10 +1051,9 @@ async function stopMinioService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort: null } })
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, dockerId: destinationDocker.id });
@ -1103,7 +1082,6 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vscodeserver');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1175,16 +1153,16 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
const changePermissionOn = persistentStorage.map((p) => p.path);
if (changePermissionOn.length > 0) {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
await executeDockerCmd({
dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' '
)}`
);
})
}
return {}
} catch ({ status, message }) {
@ -1196,9 +1174,8 @@ async function stopVscodeService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, dockerId: destinationDocker.id });
@ -1236,7 +1213,6 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const image = getServiceImage(type);
const port = getServiceMainPort('wordpress');
@ -1328,8 +1304,10 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
}
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1346,7 +1324,6 @@ async function stopWordpressService(request: FastifyRequest<ServiceStartStop>) {
wordpress: { ftpEnabled }
} = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1393,7 +1370,6 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vaultwarden');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1444,8 +1420,10 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1456,10 +1434,8 @@ async function stopVaultwardenService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1483,7 +1459,6 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('languagetool');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1536,8 +1511,9 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1548,10 +1524,8 @@ async function stopLanguageToolService(request: FastifyRequest<ServiceStartStop>
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1575,7 +1549,6 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('n8n');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1628,8 +1601,10 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1640,10 +1615,8 @@ async function stopN8nService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1667,7 +1640,6 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('uptimekuma');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1719,8 +1691,9 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1731,10 +1704,8 @@ async function stopUptimekumaService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1774,7 +1745,6 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -1871,8 +1841,9 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1883,10 +1854,8 @@ async function stopGhostService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -1917,7 +1886,6 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('meilisearch');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -1972,8 +1940,10 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -1984,10 +1954,8 @@ async function stopMeilisearchService(request: FastifyRequest<ServiceStartStop>)
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -2024,7 +1992,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('umami');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2191,8 +2158,10 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -2203,10 +2172,8 @@ async function stopUmamiService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -2245,7 +2212,6 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('hasura');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2327,8 +2293,9 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@ -2339,10 +2306,8 @@ async function stopHasuraService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -2396,7 +2361,6 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('fider');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -2489,8 +2453,8 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
@ -2502,10 +2466,8 @@ async function stopFiderService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -2554,12 +2516,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('moodle');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const domain = getDomain(fqdn);
const config = {
moodle: {
image: `${image}:${version}`,
@ -2652,8 +2612,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
@ -2665,10 +2627,8 @@ async function stopMoodleService(request: FastifyRequest<ServiceStartStop>) {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
@ -2703,14 +2663,10 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
const container = await docker.engine.getContainer(id);
const command = await container.exec({
Cmd: [
`psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
]
});
await command.start();
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could not activate users.' }
@ -2742,7 +2698,6 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
ftpHostKeyPrivate
} = data;
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
@ -2841,9 +2796,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
);
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
);
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
})
}
return reply.code(201).send({
publicPort,
@ -2856,9 +2813,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
data: { ftpPublicPort: null }
});
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
})
} catch (error) {
//
}

View File

@ -9,6 +9,7 @@ import {
getService,
getServiceLogs,
getServiceSecrets,
getServiceStatus,
getServiceStorages,
getServiceType,
getServiceUsage,
@ -41,6 +42,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request));
fastify.get<OnlyId>('/:id/status', async (request) => await getServiceStatus(request));
fastify.post<CheckService>('/:id/check', async (request) => await checkService(request));
fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));

View File

@ -250,7 +250,7 @@
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...",
"saving_and_configuring_proxy": "Saving...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"

View File

@ -41,14 +41,16 @@ export const status: Writable<any> = writable({
initialLoading: true
},
service: {
initialLoading: true,
isRunning: false,
isExited: false,
loading: false,
isRunning: false
initialLoading: true
},
database: {
initialLoading: true,
isRunning: false,
isExited: false,
loading: false,
isRunning: false
initialLoading: true
}
});
@ -60,7 +62,6 @@ export const features = readable({
export const location: Writable<null | string> = writable(null)
export const setLocation = (resource: any) => {
console.log(GITPOD_WORKSPACE_URL)
if (GITPOD_WORKSPACE_URL && resource.exposePort) {
const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href

View File

@ -36,7 +36,6 @@
return {
props: {
isQueueActive,
application
},
stuff: {
@ -53,7 +52,6 @@
<script lang="ts">
export let application: any;
export let isQueueActive: any;
import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
@ -68,7 +66,7 @@
let loading = false;
let statusInterval: any;
let isQueueActive= false;
$disabledButton =
!$appSession.isAdmin ||
!application.fqdn ||
@ -114,12 +112,12 @@
return window.location.reload();
} catch (error) {
return errorNotification(error);
}
}
}
async function getStatus() {
if ($status.application.loading) return;
$status.application.loading = true;
const data = await get(`/applications/${id}`);
const data = await get(`/applications/${id}/status`);
isQueueActive = data.isQueueActive;
$status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited;

View File

@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/applications/${params.id}/logs`);
return {
props: {
application: stuff.application,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import { page } from '$app/stores';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
@ -37,6 +18,8 @@
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@ -48,6 +31,7 @@
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/applications/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,9 +40,12 @@
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`

View File

@ -58,7 +58,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status } from '$lib/store';
import { appSession, status, disabledButton } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte';
@ -66,6 +66,9 @@
let loading = false;
let statusInterval: any = false;
$disabledButton = !$appSession.isAdmin;
async function deleteDatabase() {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) {
@ -138,6 +141,32 @@
<Loading fullscreen cover />
{:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if $status.database.isExited}
<a
href={!$disabledButton ? `/databases/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.database.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"

View File

@ -1,31 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/databases/${params.id}/logs`);
return {
props: {
database: stuff.database,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let database: any;
import { onDestroy, onMount } from 'svelte';
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { errorNotification } from '$lib/common';
const { id } = $page.params;
let loadLogsInterval: any = null;
let logs: any = [];
@ -34,9 +16,12 @@ import { errorNotification } from '$lib/common';
let followingLogs: any;
let logsEl: any;
let position = 0;
let loadingLogs = false;
let database: any = {};
const { id } = $page.params;
onMount(async () => {
const { logs: firstLogs } = await get(`/databases/${id}/logs`);
logs = firstLogs;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
@ -48,6 +33,7 @@ import { errorNotification } from '$lib/common';
});
async function loadAllLogs() {
try {
loadingLogs = true;
const data: any = await get(`/databases/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,15 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
async function loadLogs() {
if (loadingLogs) return;
try {
const newLogs: any = await get(
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
loadingLogs = true;
const newLogs: any = await get(`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
@ -70,6 +58,8 @@ import { errorNotification } from '$lib/common';
}
} catch (error) {
return errorNotification(error);
} finally {
loadingLogs = false;
}
}
function detect() {

View File

@ -14,23 +14,25 @@
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
let loadingProxy = false;
let restarting = false;
let loading = {
restart: false,
proxy: false,
save: false
};
async function handleSubmit() {
loading = true;
loading.save = true;
try {
return await post(`/destinations/${id}`, { ...destination });
await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.save = false;
}
}
onMount(async () => {
loadingProxy = true;
loading.proxy = true;
const { isRunning } = await get(`/destinations/${id}/status`);
let proxyUsed = !destination.isCoolifyProxyUsed;
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
@ -54,9 +56,10 @@
} catch (error) {
return errorNotification(error);
} finally {
loadingProxy = false;
loading.proxy = false;
}
}
loading.proxy = false;
});
async function changeProxySetting() {
if (!cannotDisable) {
@ -73,7 +76,7 @@
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
loadingProxy = true;
loading.proxy = true;
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
@ -86,7 +89,7 @@
} catch (error) {
return errorNotification(error);
} finally {
loadingProxy = false;
loading.proxy = false;
}
}
}
@ -110,7 +113,7 @@
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
@ -121,7 +124,7 @@
window.location.reload();
}, 5000);
} finally {
restarting = false;
loading.restart = false;
}
}
}
@ -134,16 +137,16 @@
<button
type="submit"
class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading.save}
disabled={loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{restarting
>{loading.restart
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
@ -185,7 +188,7 @@
{#if $appSession.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
loading={loadingProxy}
loading={loading.proxy}
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}

View File

@ -21,10 +21,9 @@
payload = {
name: $t('sources.remote_docker'),
remoteEngine: true,
ipAddress: null,
user: 'root',
port: 22,
sshPrivateKey: null,
remoteIpAddress: null,
remoteUser: 'root',
remotePort: 22,
network: cuid(),
isCoolifyProxyUsed: true
};

View File

@ -18,8 +18,7 @@
const { id } = await post(`/destinations/new`, {
...payload
});
await goto(`/destinations/${id}`);
window.location.reload();
return await goto(`/destinations/${id}`);
} catch (error) {
return errorNotification(error);
} finally {
@ -50,25 +49,25 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ipAddress" class="text-base font-bold text-stone-100"
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label
>
<input
required
name="ipAddress"
name="remoteIpAddress"
placeholder="{$t('forms.eg')}: 192.168..."
bind:value={payload.ipAddress}
bind:value={payload.remoteIpAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input required name="remoteUser" placeholder="{$t('forms.eg')}: root" bind:value={payload.remoteUser} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="remotePort" placeholder="{$t('forms.eg')}: 22" bind:value={payload.remotePort} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>

View File

@ -9,56 +9,65 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
import { errorNotification, generateRemoteEngine } from '$lib/common';
import { errorNotification } from '$lib/common';
import { appSession } from '$lib/store';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
let loadingProxy = true;
let restarting = false;
let loading = {
restart: false,
proxy: true,
save: false,
verify: false
};
$: isDisabled = !$appSession.isAdmin;
async function handleSubmit() {
loading = true;
loading.save = true;
try {
return await post(`/destinations/${id}`, { ...destination });
await post(`/destinations/${id}`, { ...destination });
toast.push('Configuration saved.');
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.save = false;
}
}
onMount(async () => {
loadingProxy = true;
const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch (error) {
return errorNotification(error);
loading.proxy = true;
if (destination.remoteEngine && destination.remoteVerified) {
const { isRunning } = await get(`/destinations/${id}/status`);
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await stopProxy();
} catch (error) {
return errorNotification(error);
}
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
await post(`/destinations/${id}/settings`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
});
await startProxy();
} catch (error) {
return errorNotification(error);
}
}
}
loadingProxy = false;
loading.proxy = false;
});
async function changeProxySetting() {
loadingProxy = true;
loading.proxy = true;
if (!cannotDisable) {
const isProxyActivated = destination.isCoolifyProxyUsed;
if (isProxyActivated) {
@ -68,7 +77,7 @@
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
);
if (!sure) {
loadingProxy = false;
loading.proxy = false;
return;
}
}
@ -87,7 +96,7 @@
} catch (error) {
return errorNotification(error);
} finally {
loadingProxy = false;
loading.proxy = false;
}
}
}
@ -111,7 +120,7 @@
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
loading.restart = true;
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart`, {
engine: destination.engine,
@ -122,18 +131,21 @@
window.location.reload();
}, 5000);
} finally {
restarting = false;
loading.restart = false;
}
}
}
async function verifyRemoteDocker() {
try {
loading = true;
return await post(`/destinations/${id}/verify`, {});
loading.verify = true;
await post(`/destinations/${id}/verify`, {});
destination.remoteVerified = true;
toast.push('Remote Docker Engine verified!');
return;
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.verify = false;
}
}
</script>
@ -145,24 +157,27 @@
<button
type="submit"
class="bg-sky-600 hover:bg-sky-500"
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? $t('forms.saving') : $t('forms.save')}
class:bg-sky-600={!loading.save}
class:hover:bg-sky-500={!loading.save}
disabled={loading.save}
>{loading.save ? $t('forms.saving') : $t('forms.save')}
</button>
{#if !destination.remoteVerified}
<button on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>Verify Remote Docker Engine</button
<button
disabled={loading.verify}
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
>{loading.verify ? 'Verifying...' : 'Verify Remote Docker Engine'}</button
>
{:else}
<button
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={loading.restart}
on:click|preventDefault={forceRestartProxy}
>{loading.restart
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
</div>
<div class="grid grid-cols-2 items-center px-10 ">
@ -232,7 +247,7 @@
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
loading={loadingProxy}
loading={loading.proxy}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title={$t('destination.use_coolify_proxy')}

View File

@ -120,8 +120,9 @@
async function getStatus() {
if ($status.service.loading) return;
$status.service.loading = true;
const data = await get(`/services/${id}`);
const data = await get(`/services/${id}/status`);
$status.service.isRunning = data.isRunning;
$status.service.isExited = data.isExited;
$status.service.initialLoading = false;
$status.service.loading = false;
}
@ -173,6 +174,32 @@
>
<div class="border border-stone-700 h-8" />
{/if}
{#if $status.service.isExited}
<a
href={!$disabledButton ? `/services/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
data-tooltip="Service exited with an error!"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
/>
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</a>
{/if}
{#if $status.service.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
@ -347,7 +374,7 @@
>
<div class="border border-stone-700 h-8" />
<a
href={$status.service.isRunning ? `/services/${id}/logs` : null}
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}

View File

@ -1,32 +1,13 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { onDestroy, onMount } from 'svelte';
export const load: Load = async ({ fetch, params, url, stuff }) => {
try {
const response = await get(`/services/${params.id}/logs`);
return {
props: {
service: stuff.service,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let service: any;
import { page } from '$app/stores';
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import { errorNotification } from '$lib/common';
import { onDestroy, onMount } from 'svelte';
let service: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
let logs: any = [];
let lastLog: any = null;
@ -36,18 +17,23 @@ import { errorNotification } from '$lib/common';
let position = 0;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/services/${id}`);
service = response.service;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/services/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
@ -56,13 +42,14 @@ import { errorNotification } from '$lib/common';
} catch (error) {
console.log(error);
return errorNotification(error);
} finally {
logsLoading = false;
}
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
);
const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);

View File

@ -38,6 +38,7 @@ importers:
eslint: 8.20.0
eslint-config-prettier: 8.5.0
eslint-plugin-prettier: 4.2.1
execa: ^6.1.0
fastify: 4.2.1
fastify-plugin: 4.0.0
generate-password: 1.7.0
@ -53,7 +54,7 @@ importers:
prettier: 2.7.1
prisma: 3.15.2
rimraf: 3.0.2
ssh-config: ^4.1.6
ssh-config: 4.1.6
strip-ansi: 7.0.1
tsconfig-paths: 4.0.0
typescript: 4.7.4
@ -77,6 +78,7 @@ importers:
dayjs: 1.11.3
dockerode: 3.3.2
dotenv-extended: 2.9.0
execa: 6.1.0
fastify: 4.2.1
fastify-plugin: 4.0.0
generate-password: 1.7.0
@ -2691,6 +2693,21 @@ packages:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false
/execa/6.1.0:
resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 3.0.1
is-stream: 3.0.0
merge-stream: 2.0.0
npm-run-path: 5.1.0
onetime: 6.0.0
signal-exit: 3.0.7
strip-final-newline: 3.0.0
dev: false
/exit/0.1.2:
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
engines: {node: '>= 0.8.0'}
@ -3171,6 +3188,11 @@ packages:
numbered: 1.1.0
dev: false
/human-signals/3.0.1:
resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==}
engines: {node: '>=12.20.0'}
dev: false
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
@ -3364,6 +3386,11 @@ packages:
engines: {node: '>=8'}
dev: false
/is-stream/3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/is-string-and-not-blank/0.0.2:
resolution: {integrity: sha512-FyPGAbNVyZpTeDCTXnzuwbu9/WpNXbCfbHXLpCRpN4GANhS00eEIP5Ef+k5HYSNIzIhdN9zRDoBj6unscECvtQ==}
engines: {node: '>=6.4.0'}
@ -3712,6 +3739,10 @@ packages:
engines: {node: '>= 0.10.0'}
dev: true
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: false
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -3754,6 +3785,11 @@ packages:
hasBin: true
dev: false
/mimic-fn/4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
dev: false
/mimic-response/1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
@ -3934,6 +3970,13 @@ packages:
string.prototype.padend: 3.1.3
dev: true
/npm-run-path/5.1.0:
resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
path-key: 4.0.0
dev: false
/numbered/1.1.0:
resolution: {integrity: sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==}
dev: false
@ -3981,6 +4024,13 @@ packages:
dependencies:
wrappy: 1.0.2
/onetime/6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
dependencies:
mimic-fn: 4.0.0
dev: false
/optionator/0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
@ -4172,6 +4222,11 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
/path-key/4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
dev: false
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@ -4768,6 +4823,10 @@ packages:
get-intrinsic: 1.1.1
object-inspect: 1.12.1
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: false
/simple-update-notifier/1.0.7:
resolution: {integrity: sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==}
engines: {node: '>=8.10.0'}
@ -4935,6 +4994,11 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
/strip-final-newline/3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
dev: false
/strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}