From 5cb9216add198fa54ca8ae4e409817ea257ca1e6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 13 Jan 2023 15:50:20 +0100 Subject: [PATCH] wip: trpc --- .../src/lib/components/SimpleExplainer.svelte | 6 + .../routes/destinations/[id]/+layout.svelte | 69 +++++ .../src/routes/destinations/[id]/+layout.ts | 45 +++ .../src/routes/destinations/[id]/+page.svelte | 18 ++ .../[id]/components/Destination.svelte | 14 + .../[id]/components/LocalDocker.svelte | 212 +++++++++++++ .../destinations/[id]/components/New.svelte | 53 ++++ .../[id]/components/NewLocalDocker.svelte | 72 +++++ .../[id]/components/NewRemoteDocker.svelte | 104 +++++++ .../[id]/components/RemoteDocker.svelte | 279 ++++++++++++++++++ apps/server/src/lib/common.ts | 113 +++++++ apps/server/src/trpc/index.ts | 6 +- .../src/trpc/routers/destinations/index.ts | 218 ++++++++++++++ apps/server/src/trpc/routers/index.ts | 1 + 14 files changed, 1208 insertions(+), 2 deletions(-) create mode 100644 apps/client/src/lib/components/SimpleExplainer.svelte create mode 100644 apps/client/src/routes/destinations/[id]/+layout.svelte create mode 100644 apps/client/src/routes/destinations/[id]/+layout.ts create mode 100644 apps/client/src/routes/destinations/[id]/+page.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/Destination.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/LocalDocker.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/New.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/NewLocalDocker.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/NewRemoteDocker.svelte create mode 100644 apps/client/src/routes/destinations/[id]/components/RemoteDocker.svelte create mode 100644 apps/server/src/trpc/routers/destinations/index.ts diff --git a/apps/client/src/lib/components/SimpleExplainer.svelte b/apps/client/src/lib/components/SimpleExplainer.svelte new file mode 100644 index 000000000..6a3198c27 --- /dev/null +++ b/apps/client/src/lib/components/SimpleExplainer.svelte @@ -0,0 +1,6 @@ + + +
{@html text}
\ No newline at end of file diff --git a/apps/client/src/routes/destinations/[id]/+layout.svelte b/apps/client/src/routes/destinations/[id]/+layout.svelte new file mode 100644 index 000000000..3f6cfa737 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/+layout.svelte @@ -0,0 +1,69 @@ + + +{#if $page.params.id !== 'new'} + +{/if} + diff --git a/apps/client/src/routes/destinations/[id]/+layout.ts b/apps/client/src/routes/destinations/[id]/+layout.ts new file mode 100644 index 000000000..e9cc53efe --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/+layout.ts @@ -0,0 +1,45 @@ +import { error } from '@sveltejs/kit'; +import { trpc } from '$lib/store'; +import type { LayoutLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; + +function checkConfiguration(destination: any): string | null { + let configurationPhase = null; + if (!destination?.remoteEngine) return configurationPhase; + if (!destination?.sshKey) { + configurationPhase = 'sshkey'; + } + return configurationPhase; +} + +export const load: LayoutLoad = async ({ params, url }) => { + const { pathname } = new URL(url); + const { id } = params; + try { + const destination = await trpc.destinations.getDestinationById.query({ id }); + if (!destination) { + throw redirect(307, '/destinations'); + } + const configurationPhase = checkConfiguration(destination); + console.log({ configurationPhase }); + // if ( + // configurationPhase && + // pathname !== `/applications/${params.id}/configuration/${configurationPhase}` + // ) { + // throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`); + // } + return { + destination + }; + } catch (err) { + if (err instanceof Error) { + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + '

' + err.message + }); + } + + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + }); + } +}; diff --git a/apps/client/src/routes/destinations/[id]/+page.svelte b/apps/client/src/routes/destinations/[id]/+page.svelte new file mode 100644 index 000000000..6b6716f52 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/+page.svelte @@ -0,0 +1,18 @@ + + +{#if id === 'new'} + +{:else} + +{/if} diff --git a/apps/client/src/routes/destinations/[id]/components/Destination.svelte b/apps/client/src/routes/destinations/[id]/components/Destination.svelte new file mode 100644 index 000000000..11135717d --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/Destination.svelte @@ -0,0 +1,14 @@ + + +
+ {#if destination.remoteEngine} + + {:else} + + {/if} +
diff --git a/apps/client/src/routes/destinations/[id]/components/LocalDocker.svelte b/apps/client/src/routes/destinations/[id]/components/LocalDocker.svelte new file mode 100644 index 000000000..237688577 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/LocalDocker.svelte @@ -0,0 +1,212 @@ + + +
+
+ + +
+
+ + + + + + + {#if $appSession.teamId === '0'} + You cannot disable this proxy as FQDN is configured for Coolify.' + : '' + }`} + /> + {/if} +
+
diff --git a/apps/client/src/routes/destinations/[id]/components/New.svelte b/apps/client/src/routes/destinations/[id]/components/New.svelte new file mode 100644 index 000000000..321ee7c70 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/New.svelte @@ -0,0 +1,53 @@ + + +
+
Add New Destination
+
+
+
Predefined destinations
+
+ + + +
+
+{#if selected === 'localDocker'} + +{:else if selected === 'remoteDocker'} + +{:else} +
Not implemented yet
+{/if} diff --git a/apps/client/src/routes/destinations/[id]/components/NewLocalDocker.svelte b/apps/client/src/routes/destinations/[id]/components/NewLocalDocker.svelte new file mode 100644 index 000000000..81eb9c0f8 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/NewLocalDocker.svelte @@ -0,0 +1,72 @@ + + +
+
+
+
Configuration
+ +
+
+ + +
+ +
+ + +
+
+ + +
+ {#if $appSession.teamId === '0'} +
+ (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} + title="Use Coolify Proxy?" + description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'} + /> +
+ {/if} +
+
diff --git a/apps/client/src/routes/destinations/[id]/components/NewRemoteDocker.svelte b/apps/client/src/routes/destinations/[id]/components/NewRemoteDocker.svelte new file mode 100644 index 000000000..5fed699d5 --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/NewRemoteDocker.svelte @@ -0,0 +1,104 @@ + + +
+ +
+
+
+
+
Configuration
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} + title="Use Coolify Proxy?" + description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'} + /> +
+
+
diff --git a/apps/client/src/routes/destinations/[id]/components/RemoteDocker.svelte b/apps/client/src/routes/destinations/[id]/components/RemoteDocker.svelte new file mode 100644 index 000000000..f67fab14e --- /dev/null +++ b/apps/client/src/routes/destinations/[id]/components/RemoteDocker.svelte @@ -0,0 +1,279 @@ + + +
+
+ {#if $appSession.isAdmin} + + + {#if destination.remoteVerified} + + {/if} + {/if} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ You cannot disable this proxy as FQDN is configured for Coolify.' + : '' + }`} + /> +
+
diff --git a/apps/server/src/lib/common.ts b/apps/server/src/lib/common.ts index 210168ebc..c55e59533 100644 --- a/apps/server/src/lib/common.ts +++ b/apps/server/src/lib/common.ts @@ -823,3 +823,116 @@ export async function startTraefikTCPProxy( }); } } +export async function startTraefikProxy(id: string): Promise { + const { engine, network, remoteEngine, remoteIpAddress } = + await prisma.destinationDocker.findUnique({ where: { id } }); + const { found } = await checkContainer({ + dockerId: id, + container: 'coolify-proxy', + remove: true + }); + const { id: settingsId, ipv4, ipv6 } = await listSettings(); + + if (!found) { + const { stdout: coolifyNetwork } = await executeCommand({ + dockerId: id, + command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` + }); + + if (!coolifyNetwork) { + await executeCommand({ + dockerId: id, + command: `docker network create --attachable coolify-infra` + }); + } + const { stdout: Config } = await executeCommand({ + dockerId: id, + command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` + }); + const ip = JSON.parse(Config)[0].Gateway; + let traefikUrl = mainTraefikEndpoint; + if (remoteEngine) { + let ip = null; + if (isDev) { + ip = getAPIUrl(); + } else { + ip = `http://${ipv4 || ipv6}:3000`; + } + traefikUrl = `${ip}/webhooks/traefik/remote/${id}`; + } + await executeCommand({ + dockerId: id, + command: `docker run --restart always \ + --add-host 'host.docker.internal:host-gateway' \ + ${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \ + -v coolify-traefik-letsencrypt:/etc/traefik/acme \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network coolify-infra \ + -p "80:80" \ + -p "443:443" \ + --name coolify-proxy \ + -d ${defaultTraefikImage} \ + --entrypoints.web.address=:80 \ + --entrypoints.web.forwardedHeaders.insecure=true \ + --entrypoints.websecure.address=:443 \ + --entrypoints.websecure.forwardedHeaders.insecure=true \ + --providers.docker=true \ + --providers.docker.exposedbydefault=false \ + --providers.http.endpoint=${traefikUrl} \ + --providers.http.pollTimeout=5s \ + --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ + --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ + --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ + --log.level=error` + }); + await prisma.destinationDocker.update({ + where: { id }, + data: { isCoolifyProxyUsed: true } + }); + } + // Configure networks for local docker engine + if (engine) { + const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); + for (const destination of destinations) { + await configureNetworkTraefikProxy(destination); + } + } + // Configure networks for remote docker engine + if (remoteEngine) { + const destinations = await prisma.destinationDocker.findMany({ where: { remoteIpAddress } }); + for (const destination of destinations) { + await configureNetworkTraefikProxy(destination); + } + } +} + +export async function configureNetworkTraefikProxy(destination: any): Promise { + const { id } = destination; + const { stdout: networks } = await executeCommand({ + dockerId: id, + command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + }); + const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); + if (!configuredNetworks.includes(destination.network)) { + await executeCommand({ + dockerId: destination.id, + command: `docker network connect ${destination.network} coolify-proxy` + }); + } +} + +export async function stopTraefikProxy(id: string): Promise<{ stdout: string; stderr: string }> { + const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' }); + await prisma.destinationDocker.update({ + where: { id }, + data: { isCoolifyProxyUsed: false } + }); + if (found) { + return await executeCommand({ + dockerId: id, + command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`, + shell: true + }); + } + return { stdout: '', stderr: '' }; +} diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index d3944f9f5..259f0fe51 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -8,7 +8,8 @@ import { applicationsRouter, servicesRouter, databasesRouter, - sourcesRouter + sourcesRouter, + destinationsRouter } from './routers'; export const appRouter = router({ @@ -18,7 +19,8 @@ export const appRouter = router({ applications: applicationsRouter, services: servicesRouter, databases: databasesRouter, - sources: sourcesRouter + sources: sourcesRouter, + destinations: destinationsRouter }); export type AppRouter = typeof appRouter; diff --git a/apps/server/src/trpc/routers/destinations/index.ts b/apps/server/src/trpc/routers/destinations/index.ts new file mode 100644 index 000000000..21d7ee957 --- /dev/null +++ b/apps/server/src/trpc/routers/destinations/index.ts @@ -0,0 +1,218 @@ +import { z } from 'zod'; +import { privateProcedure, router } from '../../trpc'; +import { + listSettings, + startTraefikProxy, + startTraefikTCPProxy, + stopTraefikProxy +} from '../../../lib/common'; +import { prisma } from '../../../prisma'; + +import { executeCommand } from '../../../lib/executeCommand'; +import { checkContainer } from '../../../lib/docker'; + +export const destinationsRouter = router({ + restartProxy: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id } = input; + await stopTraefikProxy(id); + await startTraefikProxy(id); + await prisma.destinationDocker.update({ + where: { id }, + data: { isCoolifyProxyUsed: true } + }); + }), + startProxy: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id } = input; + await startTraefikProxy(id); + }), + stopProxy: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id } = input; + await stopTraefikProxy(id); + }), + saveSettings: privateProcedure + .input( + z.object({ + id: z.string(), + engine: z.string(), + isCoolifyProxyUsed: z.boolean() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id, engine, isCoolifyProxyUsed } = input; + await prisma.destinationDocker.updateMany({ + where: { engine }, + data: { isCoolifyProxyUsed } + }); + }), + status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { + const { id } = input; + const destination = await prisma.destinationDocker.findUnique({ where: { id } }); + const { found: isRunning } = await checkContainer({ + dockerId: destination.id, + container: 'coolify-proxy', + remove: true + }); + return { + isRunning + }; + }), + save: privateProcedure + .input( + z.object({ + id: z.string(), + name: z.string(), + htmlUrl: z.string(), + apiUrl: z.string(), + customPort: z.number(), + customUser: z.string(), + isSystemWide: z.boolean().default(false) + }) + ) + .mutation(async ({ input, ctx }) => { + const { teamId } = ctx.user; + let { + id, + name, + network, + engine, + isCoolifyProxyUsed, + remoteIpAddress, + remoteUser, + remotePort + } = input; + if (id === 'new') { + if (engine) { + const { stdout } = await await executeCommand({ + command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'` + }); + if (stdout === '') { + await await executeCommand({ + command: `docker network create --attachable ${network}` + }); + } + await prisma.destinationDocker.create({ + data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed } + }); + const destinations = await prisma.destinationDocker.findMany({ where: { engine } }); + const destination = destinations.find((destination) => destination.network === network); + if (destinations.length > 0) { + const proxyConfigured = destinations.find( + (destination) => + destination.network !== network && destination.isCoolifyProxyUsed === true + ); + if (proxyConfigured) { + isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed; + } + await prisma.destinationDocker.updateMany({ + where: { engine }, + data: { isCoolifyProxyUsed } + }); + } + if (isCoolifyProxyUsed) { + await startTraefikProxy(destination.id); + } + return { id: destination.id }; + } else { + const destination = await prisma.destinationDocker.create({ + data: { + name, + teams: { connect: { id: teamId } }, + engine, + network, + isCoolifyProxyUsed, + remoteEngine: true, + remoteIpAddress, + remoteUser, + remotePort: Number(remotePort) + } + }); + return { id: destination.id }; + } + } else { + await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); + return {}; + } + }), + check: privateProcedure + .input( + z.object({ + network: z.string() + }) + ) + .query(async ({ input, ctx }) => { + const { network } = input; + const found = await prisma.destinationDocker.findFirst({ where: { network } }); + if (found) { + throw { + message: `Network already exists: ${network}` + }; + } + }), + delete: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id } = input; + const { network, remoteVerified, engine, isCoolifyProxyUsed } = + await prisma.destinationDocker.findUnique({ where: { id } }); + if (isCoolifyProxyUsed) { + if (engine || remoteVerified) { + const { stdout: found } = await executeCommand({ + dockerId: id, + command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'` + }); + if (found) { + await executeCommand({ + dockerId: id, + command: `docker network disconnect ${network} coolify-proxy` + }); + await executeCommand({ dockerId: id, command: `docker network rm ${network}` }); + } + } + } + await prisma.destinationDocker.delete({ where: { id } }); + }), + getDestinationById: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .query(async ({ input, ctx }) => { + const { id } = input; + const { teamId } = ctx.user; + const destination = await prisma.destinationDocker.findFirst({ + where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { sshKey: true, application: true, service: true, database: true } + }); + if (!destination && id !== 'new') { + throw { status: 404, message: `Destination not found.` }; + } + const settings = await listSettings(); + return { + destination, + settings + }; + }) +}); diff --git a/apps/server/src/trpc/routers/index.ts b/apps/server/src/trpc/routers/index.ts index b21ff2dca..5bc9f108b 100644 --- a/apps/server/src/trpc/routers/index.ts +++ b/apps/server/src/trpc/routers/index.ts @@ -5,3 +5,4 @@ export * from './applications'; export * from './services'; export * from './databases'; export * from './sources'; +export * from './destinations';