diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index a781bc0cb..366cce212 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -579,7 +579,8 @@ export async function executeCommand({ stream = false, buildId, applicationId, - debug + debug, + timeout = 0 }: { command: string; sshCommand?: boolean; @@ -589,6 +590,7 @@ export async function executeCommand({ buildId?: string; applicationId?: string; debug?: boolean; + timeout?: number; }): Promise> { const { execa, execaCommand } = await import('execa'); const { parse } = await import('shell-quote'); @@ -613,20 +615,26 @@ export async function executeCommand({ } if (sshCommand) { if (shell) { - return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`); + return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`, { + timeout + }); } - return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]); + return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs], { + timeout + }); } if (stream) { return await new Promise(async (resolve, reject) => { let subprocess = null; if (shell) { subprocess = execaCommand(command, { - env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, + timeout }); } else { subprocess = execa(dockerCommand, dockerArgs, { - env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, + timeout }); } const logs = []; @@ -680,19 +688,26 @@ export async function executeCommand({ } else { if (shell) { return await execaCommand(command, { - env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, + timeout }); } else { return await execa(dockerCommand, dockerArgs, { - env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }, + timeout }); } } } else { if (shell) { - return execaCommand(command, { shell: true }); + return execaCommand(command, { + shell: true, + timeout + }); } - return await execa(dockerCommand, dockerArgs); + return await execa(dockerCommand, dockerArgs, { + timeout + }); } } @@ -849,97 +864,97 @@ export function generatePassword({ type DatabaseConfiguration = | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MYSQL_DATABASE: string; - MYSQL_PASSWORD: string; - MYSQL_ROOT_USER: string; - MYSQL_USER: string; - MYSQL_ROOT_PASSWORD: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MYSQL_DATABASE: string; + MYSQL_PASSWORD: string; + MYSQL_ROOT_USER: string; + MYSQL_USER: string; + MYSQL_ROOT_PASSWORD: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MONGO_INITDB_ROOT_USERNAME?: string; - MONGO_INITDB_ROOT_PASSWORD?: string; - MONGODB_ROOT_USER?: string; - MONGODB_ROOT_PASSWORD?: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MONGO_INITDB_ROOT_USERNAME?: string; + MONGO_INITDB_ROOT_PASSWORD?: string; + MONGODB_ROOT_USER?: string; + MONGODB_ROOT_PASSWORD?: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MARIADB_ROOT_USER: string; - MARIADB_ROOT_PASSWORD: string; - MARIADB_USER: string; - MARIADB_PASSWORD: string; - MARIADB_DATABASE: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MARIADB_ROOT_USER: string; + MARIADB_ROOT_PASSWORD: string; + MARIADB_USER: string; + MARIADB_PASSWORD: string; + MARIADB_DATABASE: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - POSTGRES_PASSWORD?: string; - POSTGRES_USER?: string; - POSTGRES_DB?: string; - POSTGRESQL_POSTGRES_PASSWORD?: string; - POSTGRESQL_USERNAME?: string; - POSTGRESQL_PASSWORD?: string; - POSTGRESQL_DATABASE?: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + POSTGRES_PASSWORD?: string; + POSTGRES_USER?: string; + POSTGRES_DB?: string; + POSTGRESQL_POSTGRES_PASSWORD?: string; + POSTGRESQL_USERNAME?: string; + POSTGRESQL_PASSWORD?: string; + POSTGRESQL_DATABASE?: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - REDIS_AOF_ENABLED: string; - REDIS_PASSWORD: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + REDIS_AOF_ENABLED: string; + REDIS_PASSWORD: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - COUCHDB_PASSWORD: string; - COUCHDB_USER: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + COUCHDB_PASSWORD: string; + COUCHDB_USER: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - EDGEDB_SERVER_PASSWORD: string; - EDGEDB_SERVER_USER: string; - EDGEDB_SERVER_DATABASE: string; - EDGEDB_SERVER_TLS_CERT_MODE: string; - }; - }; + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + EDGEDB_SERVER_PASSWORD: string; + EDGEDB_SERVER_USER: string; + EDGEDB_SERVER_DATABASE: string; + EDGEDB_SERVER_TLS_CERT_MODE: string; + }; + }; export function generateDatabaseConfiguration(database: any): DatabaseConfiguration { const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } = database; @@ -1038,9 +1053,8 @@ export function generateDatabaseConfiguration(database: any): DatabaseConfigurat }; if (isARM()) { configuration.volume = `${id}-${type}-data:/data`; - configuration.command = `/usr/local/bin/redis-server --appendonly ${ - appendOnly ? 'yes' : 'no' - } --requirepass ${dbUserPassword}`; + configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no' + } --requirepass ${dbUserPassword}`; } return configuration; } else if (type === 'couchdb') { @@ -1125,12 +1139,12 @@ export type ComposeFileService = { command?: string; ports?: string[]; build?: - | { - context: string; - dockerfile: string; - args?: Record; - } - | string; + | { + context: string; + dockerfile: string; + args?: Record; + } + | string; deploy?: { restart_policy?: { condition?: string; @@ -1201,7 +1215,7 @@ export const createDirectories = async ({ let workdirFound = false; try { workdirFound = !!(await fs.stat(workdir)); - } catch (error) {} + } catch (error) { } if (workdirFound) { await executeCommand({ command: `rm -fr ${workdir}` }); } @@ -1728,7 +1742,7 @@ export async function stopBuild(buildId, applicationId) { } } count++; - } catch (error) {} + } catch (error) { } }, 100); }); } @@ -1751,7 +1765,7 @@ export async function cleanupDockerStorage(dockerId) { // Cleanup images that are not used by any container try { await executeCommand({ dockerId, command: `docker image prune -af` }); - } catch (error) {} + } catch (error) { } // Prune coolify managed containers try { @@ -1759,12 +1773,12 @@ export async function cleanupDockerStorage(dockerId) { dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }); - } catch (error) {} + } catch (error) { } // Cleanup build caches try { await executeCommand({ dockerId, command: `docker builder prune -af` }); - } catch (error) {} + } catch (error) { } } export function persistentVolumes(id, persistentStorage, config) { diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index e64ac211d..e72b46b01 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -18,6 +18,7 @@ import type { Proxy, SaveDestinationSettings } from './types'; +import { removeService } from '../../../../lib/services/common'; export async function listDestinations(request: FastifyRequest) { try { @@ -143,6 +144,35 @@ export async function newDestination(request: FastifyRequest, re return errorHandler({ status, message }); } } +export async function forceDeleteDestination(request: FastifyRequest) { + try { + const { id } = request.params; + const services = await prisma.service.findMany({ where: { destinationDockerId: id } }); + for (const service of services) { + await removeService({ id: service.id }); + } + const applications = await prisma.application.findMany({ where: { destinationDockerId: id } }); + for (const application of applications) { + await prisma.applicationSettings.deleteMany({ where: { application: { id: application.id } } }); + await prisma.buildLog.deleteMany({ where: { applicationId: application.id } }); + await prisma.build.deleteMany({ where: { applicationId: application.id } }); + await prisma.secret.deleteMany({ where: { applicationId: application.id } }); + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: application.id } }); + await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: application.id } }); + await prisma.previewApplication.deleteMany({ where: { applicationId: application.id } }); + } + const databases = await prisma.database.findMany({ where: { destinationDockerId: id } }); + for (const database of databases) { + await prisma.databaseSettings.deleteMany({ where: { databaseId: database.id } }); + await prisma.databaseSecret.deleteMany({ where: { databaseId: database.id } }); + await prisma.database.delete({ where: { id: database.id } }); + } + await prisma.destinationDocker.delete({ where: { id } }); + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} export async function deleteDestination(request: FastifyRequest) { try { const { id } = request.params; @@ -318,6 +348,7 @@ export async function verifyRemoteDockerEngineFn(id: string) { } await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }); } catch (error) { + console.log(error) throw new Error('Error while verifying remote docker engine'); } } diff --git a/apps/api/src/routes/api/v1/destinations/index.ts b/apps/api/src/routes/api/v1/destinations/index.ts index 774afa285..704c2aa79 100644 --- a/apps/api/src/routes/api/v1/destinations/index.ts +++ b/apps/api/src/routes/api/v1/destinations/index.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from 'fastify'; -import { assignSSHKey, checkDestination, deleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers'; +import { assignSSHKey, checkDestination, deleteDestination, forceDeleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers'; import type { OnlyId } from '../../../../types'; import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types'; @@ -14,6 +14,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id', async (request) => await getDestination(request)); fastify.post('/:id', async (request, reply) => await newDestination(request, reply)); fastify.delete('/:id', async (request) => await deleteDestination(request)); + fastify.delete('/:id/force', async (request) => await forceDeleteDestination(request)); fastify.get('/:id/status', async (request) => await getDestinationStatus(request)); fastify.post('/:id/settings', async (request) => await saveDestinationSettings(request)); diff --git a/apps/ui/src/routes/destinations/[id]/__layout.svelte b/apps/ui/src/routes/destinations/[id]/__layout.svelte index 475dbc342..a0f80137f 100644 --- a/apps/ui/src/routes/destinations/[id]/__layout.svelte +++ b/apps/ui/src/routes/destinations/[id]/__layout.svelte @@ -75,6 +75,25 @@ } } } + async function forceDeleteDestination(destination: any) { + let sure = confirm($t('application.confirm_to_delete', { name: destination.name })); + if (sure) { + sure = confirm( + 'Are you REALLY sure? This will delete all resources associated with this destination, but not on the destination (server) itself. You will have manually delete everything on the server afterwards.' + ); + if (sure) { + sure = confirm('REALLY?'); + if (sure) { + try { + await del(`/destinations/${destination.id}/force`, { id: destination.id }); + return await goto('/', { replaceState: true }); + } catch (error) { + return errorNotification(error); + } + } + } + } + } function deletable() { if (!isDestinationDeletable) { return 'Please delete all resources before deleting this.'; @@ -88,7 +107,7 @@ {#if $page.params.id !== 'new'} -