From 3cdb6f218d13e3fe014d559b14caa98268e066e1 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Wed, 29 Jan 2025 11:25:26 +0000 Subject: [PATCH] feat: created base class BaseDbToolService to share common code with other DB Tool Services --- src/app/project/[projectId]/actions.ts | 4 +- src/app/project/app/[appId]/app-tabs.tsx | 4 +- .../app/[appId]/credentials/actions.ts | 34 ++- .../app/[appId]/credentials/db-gate.tsx | 82 +++-- .../app/[appId]/credentials/db-tools.tsx | 22 ++ .../db-tool-services/base-db-tool.service.ts | 182 +++++++++++ .../db-tool-services/dbgate.service.ts | 139 +++++++++ src/server/services/dbgate.service.ts | 286 ------------------ 8 files changed, 407 insertions(+), 346 deletions(-) create mode 100644 src/app/project/app/[appId]/credentials/db-tools.tsx create mode 100644 src/server/services/db-tool-services/base-db-tool.service.ts create mode 100644 src/server/services/db-tool-services/dbgate.service.ts delete mode 100644 src/server/services/dbgate.service.ts diff --git a/src/app/project/[projectId]/actions.ts b/src/app/project/[projectId]/actions.ts index 96dc66b..971b2d5 100644 --- a/src/app/project/[projectId]/actions.ts +++ b/src/app/project/[projectId]/actions.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import appTemplateService from "@/server/services/app-template.service"; import { AppTemplateModel, appTemplateZodModel } from "@/shared/model/app-template.model"; import { ServiceException } from "@/shared/model/service.exception.model"; -import dbGateService from "@/server/services/dbgate.service"; +import dbGateService from "@/server/services/db-tool-services/dbgate.service"; import fileBrowserService from "@/server/services/file-browser-service"; const createAppSchema = z.object({ @@ -42,7 +42,7 @@ export const deleteApp = async (appId: string) => await getAuthUserSession(); const app = await appService.getExtendedById(appId); // First delete external services wich might be running - await dbGateService.deleteDbGatDeploymentForAppIfExists(appId); + await dbGateService.deleteToolForAppIfExists(appId); for (const volume of app.appVolumes) { await fileBrowserService.deleteFileBrowserForVolumeIfExists(volume.id); } diff --git a/src/app/project/app/[appId]/app-tabs.tsx b/src/app/project/app/[appId]/app-tabs.tsx index c8bb8cd..779786c 100644 --- a/src/app/project/app/[appId]/app-tabs.tsx +++ b/src/app/project/app/[appId]/app-tabs.tsx @@ -20,7 +20,7 @@ import VolumeBackupList from "./volumes/volume-backup"; import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended.model"; import BasicAuth from "./advanced/basic-auth"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import DbGateCard from "./credentials/db-gate"; +import DbToolsCard from "./credentials/db-tools"; export default function AppTabs({ app, @@ -61,7 +61,7 @@ export default function AppTabs({ {app.appType !== 'APP' && - + } diff --git a/src/app/project/app/[appId]/credentials/actions.ts b/src/app/project/app/[appId]/credentials/actions.ts index 48e068f..9f15ac8 100644 --- a/src/app/project/app/[appId]/credentials/actions.ts +++ b/src/app/project/app/[appId]/credentials/actions.ts @@ -1,11 +1,12 @@ 'use server' import appService from "@/server/services/app.service"; -import dbGateService from "@/server/services/dbgate.service"; +import dbGateService from "@/server/services/db-tool-services/dbgate.service"; import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils"; import { AppTemplateUtils } from "@/server/utils/app-template.utils"; import { DatabaseTemplateInfoModel } from "@/shared/model/database-template-info.model"; import { ServerActionResult, SuccessActionResult } from "@/shared/model/server-action-error-return.model"; +import { ServiceException } from "@/shared/model/service.exception.model"; export const getDatabaseCredentials = async (appId: string) => simpleAction(async () => { @@ -18,29 +19,40 @@ export const getDatabaseCredentials = async (appId: string) => export const getIsDbGateActive = async (appId: string) => simpleAction(async () => { await getAuthUserSession(); - const isActive = await dbGateService.isDbGateRunning(appId); + const isActive = await dbGateService.isDbToolRunning(appId); return new SuccessActionResult(isActive); }) as Promise>; -export const deployDbGate = async (appId: string) => +export const deployDbTool = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin') => simpleAction(async () => { await getAuthUserSession(); - await dbGateService.deployDbGateForDatabase(appId); - return new SuccessActionResult(); + if (dbTool === 'dbgate') { + await dbGateService.deploy(appId); + return new SuccessActionResult(); + } else { + throw new ServiceException('Unknown db tool'); + } }) as Promise>; -export const getLoginCredentialsForRunningDbGate = async (appId: string) => +export const getLoginCredentialsForRunningDbTool = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin') => simpleAction(async () => { await getAuthUserSession(); - const credentials = await dbGateService.getLoginCredentialsForRunningDbGate(appId); - return new SuccessActionResult(credentials); + if (dbTool === 'dbgate') { + return new SuccessActionResult(await dbGateService.getLoginCredentialsForRunningDbGate(appId)); + } else { + throw new ServiceException('Unknown db tool'); + } }) as Promise>; -export const deleteDbGatDeploymentForAppIfExists = async (appId: string) => +export const deleteDbToolDeploymentForAppIfExists = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin') => simpleAction(async () => { await getAuthUserSession(); - await dbGateService.deleteDbGatDeploymentForAppIfExists(appId); - return new SuccessActionResult(); + if (dbTool === 'dbgate') { + await dbGateService.deleteToolForAppIfExists(appId); + return new SuccessActionResult(); + } else { + throw new ServiceException('Unknown db tool'); + } }) as Promise>; export const downloadDbGateFilesForApp = async (appId: string) => diff --git a/src/app/project/app/[appId]/credentials/db-gate.tsx b/src/app/project/app/[appId]/credentials/db-gate.tsx index 60c00d6..47a7ffb 100644 --- a/src/app/project/app/[appId]/credentials/db-gate.tsx +++ b/src/app/project/app/[appId]/credentials/db-gate.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { useConfirmDialog } from "@/frontend/states/zustand.states"; import { Toast } from "@/frontend/utils/toast.utils"; import { Actions } from "@/frontend/utils/nextjs-actions.utils"; -import { deleteDbGatDeploymentForAppIfExists, deployDbGate, downloadDbGateFilesForApp, getIsDbGateActive, getLoginCredentialsForRunningDbGate } from "./actions"; +import { deleteDbToolDeploymentForAppIfExists, deployDbTool, downloadDbGateFilesForApp, getIsDbGateActive, getLoginCredentialsForRunningDbTool } from "./actions"; import { Label } from "@/components/ui/label"; import FullLoadingSpinner from "@/components/ui/full-loading-spinnter"; import { Switch } from "@/components/ui/switch"; @@ -14,7 +14,7 @@ import LoadingSpinner from "@/components/ui/loading-spinner"; import { Download } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -export default function DbGateCard({ +export default function DbGateDbTool({ app }: { app: AppExtendedModel; @@ -45,7 +45,7 @@ export default function DbGateCard({ const openDbGateAsync = async () => { try { setLoading(true); - const credentials = await Actions.run(() => getLoginCredentialsForRunningDbGate(app.id)); + const credentials = await Actions.run(() => getLoginCredentialsForRunningDbTool(app.id, 'dbgate')); setLoading(false); await openConfirmDialog({ title: "Open DB Gate", @@ -80,48 +80,40 @@ export default function DbGateCard({ }, [app]); return <> - - - Database Access - Activate one of the following tools to access the database through your browser. - - - {isDbGateActive === undefined ? :
-
- { - try { - setLoading(true); - if (checked) { - await Toast.fromAction(() => deployDbGate(app.id), 'DB Gate is now activated', 'Activating DB Gate...'); - } else { - await Toast.fromAction(() => deleteDbGatDeploymentForAppIfExists(app.id), 'DB Gate has been deactivated', 'Deactivating DB Gate...'); - } - await loadIsDbGateActive(app.id); - } finally { - setLoading(false); - } - }} /> - -
- {isDbGateActive && <> - + {isDbGateActive === undefined ? :
+
+ { + try { + setLoading(true); + if (checked) { + await Toast.fromAction(() => deployDbTool(app.id, 'dbgate'), 'DB Gate is now activated', 'Activating DB Gate...'); + } else { + await Toast.fromAction(() => deleteDbToolDeploymentForAppIfExists(app.id, 'dbgate'), 'DB Gate has been deactivated', 'Deactivating DB Gate...'); + } + await loadIsDbGateActive(app.id); + } finally { + setLoading(false); + } + }} /> + +
+ {isDbGateActive && <> + - - - - - - -

Download the "Files" folder from DB Gate.

-
-
-
- } - {loading && } -
} - - + + + + + + +

Download the "Files" folder from DB Gate.

+
+
+
+ } + {loading && } +
} ; } diff --git a/src/app/project/app/[appId]/credentials/db-tools.tsx b/src/app/project/app/[appId]/credentials/db-tools.tsx new file mode 100644 index 0000000..4aa40d9 --- /dev/null +++ b/src/app/project/app/[appId]/credentials/db-tools.tsx @@ -0,0 +1,22 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { AppExtendedModel } from "@/shared/model/app-extended.model"; +import DbGateDbTool from "./db-gate"; + +export default function DbToolsCard({ + app +}: { + app: AppExtendedModel; +}) { + + return <> + + + Database Access + Activate one of the following tools to access the database through your browser. + + + + + + ; +} diff --git a/src/server/services/db-tool-services/base-db-tool.service.ts b/src/server/services/db-tool-services/base-db-tool.service.ts new file mode 100644 index 0000000..d05c5e4 --- /dev/null +++ b/src/server/services/db-tool-services/base-db-tool.service.ts @@ -0,0 +1,182 @@ +import { ServiceException } from "@/shared/model/service.exception.model"; +import dataAccess from "../../adapter/db.client"; +import traefikMeDomainService from "../traefik-me-domain.service"; +import { KubeObjectNameUtils } from "../../utils/kube-object-name.utils"; +import deploymentService from "../deployment.service"; +import { V1Deployment, V1Ingress } from "@kubernetes/client-node"; +import { Constants } from "@/shared/utils/constants"; +import k3s from "../../adapter/kubernetes-api.adapter"; +import ingressService from "../ingress.service"; +import svcService from "../svc.service"; +import podService from "../pod.service"; +import appService from "../app.service"; +import { AppExtendedModel } from "@/shared/model/app-extended.model"; + +export class BaseDbToolService { + + appIdToToolNameConverter: (appId: string) => string; + + constructor(appIdToToolNameConverter: (appId: string) => string) { + this.appIdToToolNameConverter = appIdToToolNameConverter; + } + + async isDbToolRunning(appId: string) { + const toolAppName = this.appIdToToolNameConverter(appId); + const app = await appService.getExtendedById(appId); + const projectId = app.projectId; + + const existingDeployment = await deploymentService.getDeployment(projectId, toolAppName); + if (!existingDeployment) { + return false; + } + + const existingService = await svcService.getService(projectId, toolAppName); + if (!existingService) { + return false; + } + + const existingIngress = await ingressService.getIngressByName(projectId, toolAppName); + if (!existingIngress) { + return false; + } + + return true; + } + + async getLoginCredentialsForRunningTool(appId: string, + searchFunc: (existingDeployment: V1Deployment, app: AppExtendedModel) => { username: string, password: string }) { + const app = await appService.getExtendedById(appId); + const toolAppName = this.appIdToToolNameConverter(appId); + const projectId = app.projectId; + + const isDbGateRunning = await this.isDbToolRunning(appId); + if (!isDbGateRunning) { + throw new ServiceException('DB Gate is not running for this database'); + } + + const existingDeployment = await deploymentService.getDeployment(projectId, toolAppName); + if (!existingDeployment) { + throw new ServiceException('DB Gate is not running for this database'); + } + + const { username, password } = searchFunc(existingDeployment, app); + const traefikHostname = await traefikMeDomainService.getDomainForApp(toolAppName); + return { url: `https://${traefikHostname}`, username, password }; + } + + async deployToolForDatabase(appId: string, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment) { + const app = await appService.getExtendedById(appId); + const toolAppName = this.appIdToToolNameConverter(appId); + + if (app.appType === 'APP') { + throw new ServiceException(`The DB Tool ${toolAppName} can only be deployed for databases, not for apps`); + } + + const namespace = app.projectId; + + console.log(`Deploying DB Tool ${toolAppName} for app ${appId}`); + const traefikHostname = await traefikMeDomainService.getDomainForApp(toolAppName); + + console.log(`Creating DB Tool ${toolAppName} deployment for app ${appId}`); + await this.createOrUpdateDbGateDeployment(app, deplyomentBuilder); + + console.log(`Creating service for DB Tool ${toolAppName} for app ${appId}`); + await svcService.createOrUpdateService(namespace, toolAppName, [{ + name: 'http', + port: 80, + targetPort: 3000, + }]); + + console.log(`Creating ingress for DB Tool ${toolAppName} for app ${appId}`); + await this.createOrUpdateIngress(toolAppName, namespace, traefikHostname); + + const fileBrowserPods = await podService.getPodsForApp(namespace, toolAppName); + for (const pod of fileBrowserPods) { + await podService.waitUntilPodIsRunningFailedOrSucceded(namespace, pod.podName); + } + } + + + private async createOrUpdateDbGateDeployment(app: AppExtendedModel, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment) { + const body = deplyomentBuilder(app); + const toolAppName = this.appIdToToolNameConverter(app.id); + await deploymentService.applyDeployment(app.projectId, toolAppName, body); + } + + async deleteToolForAppIfExists(appId: string) { + const app = await dataAccess.client.app.findFirst({ + where: { + id: appId + } + }); + + if (!app) { + return; + } + + const toolAppName = this.appIdToToolNameConverter(appId); + const projectId = app.projectId; + + const existingDeployment = await deploymentService.getDeployment(projectId, toolAppName); + if (existingDeployment) { await k3s.apps.deleteNamespacedDeployment(toolAppName, projectId); } + + const existingService = await svcService.getService(projectId, toolAppName); + if (existingService) { await svcService.deleteService(projectId, toolAppName); } + + const existingIngress = await ingressService.getIngressByName(projectId, toolAppName); + if (existingIngress) { + await k3s.network.deleteNamespacedIngress(KubeObjectNameUtils.getIngressName(toolAppName), projectId); + } + } + + private async createOrUpdateIngress(dbGateAppName: string, namespace: string, traefikHostname: string) { + const ingressDefinition: V1Ingress = { + apiVersion: 'networking.k8s.io/v1', + kind: 'Ingress', + metadata: { + name: KubeObjectNameUtils.getIngressName(dbGateAppName), + namespace: namespace, + // dont annotate, because ingress will be deleted after redeployment of app + /* annotations: { + [Constants.QS_ANNOTATION_APP_ID]: appId, + [Constants.QS_ANNOTATION_PROJECT_ID]: projectId, + },*/ + }, + spec: { + ingressClassName: 'traefik', + rules: [ + { + host: traefikHostname, + http: { + paths: [ + { + path: '/', + pathType: 'Prefix', + backend: { + service: { + name: KubeObjectNameUtils.toServiceName(dbGateAppName), + port: { + number: 80, + }, + }, + }, + }, + ], + }, + }, + ], + tls: [{ + hosts: [traefikHostname], + secretName: Constants.TRAEFIK_ME_SECRET_NAME, + }], + }, + }; + + const existingIngress = await ingressService.getIngressByName(namespace, dbGateAppName); + if (existingIngress) { + await k3s.network.replaceNamespacedIngress(KubeObjectNameUtils.getIngressName(dbGateAppName), namespace, ingressDefinition); + } else { + await k3s.network.createNamespacedIngress(namespace, ingressDefinition); + } + } +} diff --git a/src/server/services/db-tool-services/dbgate.service.ts b/src/server/services/db-tool-services/dbgate.service.ts new file mode 100644 index 0000000..e1d0609 --- /dev/null +++ b/src/server/services/db-tool-services/dbgate.service.ts @@ -0,0 +1,139 @@ +import { ServiceException } from "@/shared/model/service.exception.model"; +import { KubeObjectNameUtils } from "../../utils/kube-object-name.utils"; +import { randomBytes } from "crypto"; +import { V1Deployment, V1EnvVar } from "@kubernetes/client-node"; +import { Constants } from "@/shared/utils/constants"; +import podService from "../pod.service"; +import { AppTemplateUtils } from "../../utils/app-template.utils"; +import appService from "../app.service"; +import { PathUtils } from "../../utils/path.utils"; +import { FsUtils } from "../../utils/fs.utils"; +import path from "path"; +import { BaseDbToolService } from "./base-db-tool.service"; + +class DbGateService extends BaseDbToolService { + + constructor() { + super((app) => KubeObjectNameUtils.toDbGateId(app)); + } + + async downloadDbGateFilesForApp(appId: string) { + + const app = await appService.getExtendedById(appId); + const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); + const pod = await podService.getPodsForApp(app.projectId, dbGateAppName); + if (pod.length === 0) { + throw new ServiceException(`There are no running pods for DBGate. Make sure the DB Gate is running.`); + } + const firstPod = pod[0]; + + const continerSourcePath = '/root/.dbgate/files'; + const continerRootPath = '/root'; + + await podService.runCommandInPod(app.projectId, firstPod.podName, firstPod.containerName, ['cp', '-r', continerSourcePath, continerRootPath]); + + const downloadPath = path.join(PathUtils.tempVolumeDownloadPath, dbGateAppName + '.tar.gz'); + await FsUtils.createDirIfNotExistsAsync(PathUtils.tempVolumeDownloadPath, true); + await FsUtils.deleteDirIfExistsAsync(downloadPath, true); + + console.log(`Downloading data from pod ${firstPod.podName} ${continerRootPath} to ${downloadPath}`); + await podService.cpFromPod(app.projectId, firstPod.podName, firstPod.containerName, continerRootPath, downloadPath, continerRootPath); + + const fileName = path.basename(downloadPath); + return fileName; + } + + + async getLoginCredentialsForRunningDbGate(appId: string) { + return await this.getLoginCredentialsForRunningTool(appId, (existingDeployment) => { + const username = existingDeployment.spec?.template.spec?.containers[0].env?.find(e => e.name === 'LOGIN')?.value; + const password = existingDeployment.spec?.template.spec?.containers[0].env?.find(e => e.name === 'PASSWORD')?.value; + if (!username || !password) { + throw new ServiceException('Could not find login credentials for DB Gate, please restart DB Gate'); + } + return { username, password }; + }); + } + + async deploy(appId: string) { + await this.deployToolForDatabase(appId, (app) => { + const authPassword = randomBytes(15).toString('hex'); + const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); + const projectId = app.projectId; + + const dbCredentials = AppTemplateUtils.getDatabaseModelFromApp(app); + const connectionId = 'qsdb'; + const envVars: V1EnvVar[] = [ + { name: 'LOGIN', value: 'quickstack' }, + { name: 'PASSWORD', value: authPassword }, + + { name: 'CONNECTIONS', value: connectionId }, + { name: `LABEL_${connectionId}`, value: app.name }, + { name: `SERVER_${connectionId}`, value: dbCredentials.hostname }, + { name: `USER_${connectionId}`, value: dbCredentials.username }, + { name: `PORT_${connectionId}`, value: dbCredentials.port + '' }, + { name: `PASSWORD_${connectionId}`, value: dbCredentials.password }, + ]; + if (app.appType === 'POSTGRES') { + envVars.push(...[ + { name: `ENGINE_${connectionId}`, value: 'postgres@dbgate-plugin-postgres' }, + ]); + } else if (app.appType === 'MYSQL') { + envVars.push(...[ + { name: `ENGINE_${connectionId}`, value: 'mysql@dbgate-plugin-mysql' }, + ]); + } else if (app.appType === 'MARIADB') { + envVars.push(...[ + { name: `ENGINE_${connectionId}`, value: 'mariadb@dbgate-plugin-mysql' }, + ]); + } else if (app.appType === 'MONGODB') { + envVars.push(...[ + { name: `ENGINE_${connectionId}`, value: 'mongo@dbgate-plugin-mongo' }, + ]); + } else { + throw new ServiceException('QuickStack does not support this app type'); + } + + const body: V1Deployment = { + metadata: { + name: dbGateAppName + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: dbGateAppName + } + }, + template: { + metadata: { + labels: { + app: dbGateAppName + }, + annotations: { + [Constants.QS_ANNOTATION_APP_ID]: app.id, + [Constants.QS_ANNOTATION_PROJECT_ID]: projectId, + deploymentTimestamp: new Date().getTime() + "", + "kubernetes.io/change-cause": `Deployment ${new Date().toISOString()}` + } + }, + spec: { + containers: [ + { + name: dbGateAppName, + image: 'dbgate/dbgate:latest', + imagePullPolicy: 'Always', + env: envVars + } + ], + } + } + } + }; + return body; + }); + } +} + +const dbGateService = new DbGateService(); +export default dbGateService; \ No newline at end of file diff --git a/src/server/services/dbgate.service.ts b/src/server/services/dbgate.service.ts deleted file mode 100644 index f13f45e..0000000 --- a/src/server/services/dbgate.service.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { ServiceException } from "@/shared/model/service.exception.model"; -import dataAccess from "../adapter/db.client"; -import traefikMeDomainService from "./traefik-me-domain.service"; -import { KubeObjectNameUtils } from "../utils/kube-object-name.utils"; -import { randomBytes } from "crypto"; -import deploymentService from "./deployment.service"; -import { V1Deployment, V1EnvVar, V1Ingress } from "@kubernetes/client-node"; -import { Constants } from "@/shared/utils/constants"; -import k3s from "../adapter/kubernetes-api.adapter"; -import ingressService from "./ingress.service"; -import svcService from "./svc.service"; -import podService from "./pod.service"; -import { AppTemplateUtils } from "../utils/app-template.utils"; -import appService from "./app.service"; -import { AppExtendedModel } from "@/shared/model/app-extended.model"; -import { PathUtils } from "../utils/path.utils"; -import { FsUtils } from "../utils/fs.utils"; -import path from "path"; - -class DbGateService { - - async isDbGateRunning(appId: string) { - const app = await appService.getExtendedById(appId); - const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); - const projectId = app.projectId; - - const existingDeployment = await deploymentService.getDeployment(projectId, dbGateAppName); - if (!existingDeployment) { - return false; - } - - const existingService = await svcService.getService(projectId, dbGateAppName); - if (!existingService) { - return false; - } - - const existingIngress = await ingressService.getIngressByName(projectId, dbGateAppName); - if (!existingIngress) { - return false; - } - - return true; - } - - async downloadDbGateFilesForApp(appId: string) { - - const app = await appService.getExtendedById(appId); - const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); - const pod = await podService.getPodsForApp(app.projectId, dbGateAppName); - if (pod.length === 0) { - throw new ServiceException(`There are no running pods for DBGate. Make sure the DB Gate is running.`); - } - const firstPod = pod[0]; - - const continerSourcePath = '/root/.dbgate/files'; - const continerRootPath = '/root'; - - await podService.runCommandInPod(app.projectId, firstPod.podName, firstPod.containerName, ['cp', '-r', continerSourcePath, continerRootPath]); - - const downloadPath = path.join(PathUtils.tempVolumeDownloadPath, dbGateAppName + '.tar.gz'); - await FsUtils.createDirIfNotExistsAsync(PathUtils.tempVolumeDownloadPath, true); - await FsUtils.deleteDirIfExistsAsync(downloadPath, true); - - console.log(`Downloading data from pod ${firstPod.podName} ${continerRootPath} to ${downloadPath}`); - await podService.cpFromPod(app.projectId, firstPod.podName, firstPod.containerName, continerRootPath, downloadPath, continerRootPath); - - const fileName = path.basename(downloadPath); - return fileName; - } - - async getLoginCredentialsForRunningDbGate(appId: string) { - const app = await appService.getExtendedById(appId); - const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); - const projectId = app.projectId; - - const isDbGateRunning = await this.isDbGateRunning(appId); - if (!isDbGateRunning) { - throw new ServiceException('DB Gate is not running for this database'); - } - - const existingDeployment = await deploymentService.getDeployment(projectId, dbGateAppName); - if (!existingDeployment) { - throw new ServiceException('DB Gate is not running for this database'); - } - - const username = existingDeployment.spec?.template.spec?.containers[0].env?.find(e => e.name === 'LOGIN')?.value; - const password = existingDeployment.spec?.template.spec?.containers[0].env?.find(e => e.name === 'PASSWORD')?.value; - - const traefikHostname = await traefikMeDomainService.getDomainForApp(dbGateAppName); - - return { url: `https://${traefikHostname}`, username, password }; - } - - async deployDbGateForDatabase(appId: string) { - const app = await appService.getExtendedById(appId); - - if (app.appType === 'APP') { - throw new ServiceException('DB Gate can only be deployed for databases, not for apps'); - } - - const namespace = app.projectId; - const dbGateAppId = KubeObjectNameUtils.toDbGateId(appId); - - console.log(`Deploying DBGate for app ${appId}`); - const traefikHostname = await traefikMeDomainService.getDomainForApp(dbGateAppId); - - console.log(`Creating DBGate deployment for app ${appId}`); - - const randomPassword = randomBytes(15).toString('hex'); - await this.createOrUpdateDbGateDeployment(app, randomPassword); - - console.log(`Creating service for DBGate for app ${appId}`); - await svcService.createOrUpdateService(namespace, dbGateAppId, [{ - name: 'http', - port: 80, - targetPort: 3000, - }]); - - console.log(`Creating ingress for DBGate for app ${appId}`); - await this.createOrUpdateIngress(dbGateAppId, namespace, appId, namespace, traefikHostname); - - const fileBrowserPods = await podService.getPodsForApp(namespace, dbGateAppId); - for (const pod of fileBrowserPods) { - await podService.waitUntilPodIsRunningFailedOrSucceded(namespace, pod.podName); - } - - return { url: `https://${traefikHostname}`, password: randomPassword }; - } - - async deleteDbGatDeploymentForAppIfExists(appId: string) { - const app = await dataAccess.client.app.findFirst({ - where: { - id: appId - } - }); - - if (!app) { - return; - } - - const kubeAppName = KubeObjectNameUtils.toDbGateId(appId); - const projectId = app.projectId; - - const existingDeployment = await deploymentService.getDeployment(projectId, kubeAppName); - if (existingDeployment) { await k3s.apps.deleteNamespacedDeployment(kubeAppName, projectId); } - - const existingService = await svcService.getService(projectId, kubeAppName); - if (existingService) { await svcService.deleteService(projectId, kubeAppName); } - - const existingIngress = await ingressService.getIngressByName(projectId, kubeAppName); - if (existingIngress) { - await k3s.network.deleteNamespacedIngress(KubeObjectNameUtils.getIngressName(kubeAppName), projectId); - } - } - - private async createOrUpdateIngress(dbGateAppName: string, namespace: string, appId: string, projectId: string, traefikHostname: string) { - const ingressDefinition: V1Ingress = { - apiVersion: 'networking.k8s.io/v1', - kind: 'Ingress', - metadata: { - name: KubeObjectNameUtils.getIngressName(dbGateAppName), - namespace: namespace, - annotations: { - [Constants.QS_ANNOTATION_APP_ID]: appId, - [Constants.QS_ANNOTATION_PROJECT_ID]: projectId, - }, - }, - spec: { - ingressClassName: 'traefik', - rules: [ - { - host: traefikHostname, - http: { - paths: [ - { - path: '/', - pathType: 'Prefix', - backend: { - service: { - name: KubeObjectNameUtils.toServiceName(dbGateAppName), - port: { - number: 80, - }, - }, - }, - }, - ], - }, - }, - ], - tls: [{ - hosts: [traefikHostname], - secretName: Constants.TRAEFIK_ME_SECRET_NAME, - }], - }, - }; - - const existingIngress = await ingressService.getIngressByName(projectId, dbGateAppName); - if (existingIngress) { - await k3s.network.replaceNamespacedIngress(KubeObjectNameUtils.getIngressName(dbGateAppName), projectId, ingressDefinition); - } else { - await k3s.network.createNamespacedIngress(projectId, ingressDefinition); - } - } - - private async createOrUpdateDbGateDeployment(app: AppExtendedModel, authPassword: string) { - - const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id); - const projectId = app.projectId; - - const dbCredentials = AppTemplateUtils.getDatabaseModelFromApp(app); - const connectionId = 'qsdb'; - const envVars: V1EnvVar[] = [ - { name: 'LOGIN', value: 'quickstack' }, - { name: 'PASSWORD', value: authPassword }, - - { name: 'CONNECTIONS', value: connectionId }, - { name: `LABEL_${connectionId}`, value: app.name }, - { name: `SERVER_${connectionId}`, value: dbCredentials.hostname }, - { name: `USER_${connectionId}`, value: dbCredentials.username }, - { name: `PORT_${connectionId}`, value: dbCredentials.port + '' }, - { name: `PASSWORD_${connectionId}`, value: dbCredentials.password }, - ]; - if (app.appType === 'POSTGRES') { - envVars.push(...[ - { name: `ENGINE_${connectionId}`, value: 'postgres@dbgate-plugin-postgres' }, - ]); - } else if (app.appType === 'MYSQL') { - envVars.push(...[ - { name: `ENGINE_${connectionId}`, value: 'mysql@dbgate-plugin-mysql' }, - ]); - } else if (app.appType === 'MARIADB') { - envVars.push(...[ - { name: `ENGINE_${connectionId}`, value: 'mariadb@dbgate-plugin-mysql' }, - ]); - } else if (app.appType === 'MONGODB') { - envVars.push(...[ - { name: `ENGINE_${connectionId}`, value: 'mongo@dbgate-plugin-mongo' }, - ]); - } else { - throw new ServiceException('QuickStack does not support this app type'); - } - - const body: V1Deployment = { - metadata: { - name: dbGateAppName - }, - spec: { - replicas: 1, - selector: { - matchLabels: { - app: dbGateAppName - } - }, - template: { - metadata: { - labels: { - app: dbGateAppName - }, - annotations: { - [Constants.QS_ANNOTATION_APP_ID]: app.id, - [Constants.QS_ANNOTATION_PROJECT_ID]: projectId, - deploymentTimestamp: new Date().getTime() + "", - "kubernetes.io/change-cause": `Deployment ${new Date().toISOString()}` - } - }, - spec: { - containers: [ - { - name: dbGateAppName, - image: 'dbgate/dbgate:latest', - imagePullPolicy: 'Always', - env: envVars - } - ], - } - } - } - }; - - await deploymentService.applyDeployment(projectId, dbGateAppName, body); - } -} - -const dbGateService = new DbGateService(); -export default dbGateService; \ No newline at end of file