feat: created base class BaseDbToolService to share common code with other DB Tool Services

This commit is contained in:
biersoeckli
2025-01-29 11:25:26 +00:00
parent 9f4bff24c0
commit 3cdb6f218d
8 changed files with 407 additions and 346 deletions

View File

@@ -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);
}

View File

@@ -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({
<WebhookDeploymentInfo app={app} />
</TabsContent>
{app.appType !== 'APP' && <TabsContent value="credentials" className="space-y-4">
<DbGateCard app={app} />
<DbToolsCard app={app} />
<DbCredentials app={app} />
</TabsContent>}
<TabsContent value="general" className="space-y-4">

View File

@@ -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<ServerActionResult<unknown, boolean>>;
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<ServerActionResult<unknown, void>>;
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<ServerActionResult<unknown, { url: string; username: string, password: string }>>;
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<ServerActionResult<unknown, void>>;
export const downloadDbGateFilesForApp = async (appId: string) =>

View File

@@ -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 <>
<Card>
<CardHeader>
<CardTitle>Database Access</CardTitle>
<CardDescription>Activate one of the following tools to access the database through your browser.</CardDescription>
</CardHeader>
<CardContent>
{isDbGateActive === undefined ? <FullLoadingSpinner /> : <div className="flex gap-4 items-center">
<div className="flex items-center space-x-2">
<Switch id="canary-channel-mode" disabled={loading} checked={isDbGateActive} onCheckedChange={async (checked) => {
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);
}
}} />
<Label htmlFor="airplane-mode">DB Gate</Label>
</div>
{isDbGateActive && <>
<Button variant='outline' onClick={() => openDbGateAsync()}
disabled={!isDbGateActive || loading}>Open DB Gate</Button>
{isDbGateActive === undefined ? <FullLoadingSpinner /> : <div className="flex gap-4 items-center">
<div className="flex items-center space-x-2">
<Switch id="canary-channel-mode" disabled={loading} checked={isDbGateActive} onCheckedChange={async (checked) => {
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);
}
}} />
<Label htmlFor="airplane-mode">DB Gate</Label>
</div>
{isDbGateActive && <>
<Button variant='outline' onClick={() => openDbGateAsync()}
disabled={!isDbGateActive || loading}>Open DB Gate</Button>
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger>
<Button onClick={() => downloadDbGateFilesForAppAsync()} disabled={!isDbGateActive || loading}
variant="ghost"><Download /></Button>
</TooltipTrigger>
<TooltipContent>
<p>Download the "Files" folder from DB Gate.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>}
{loading && <LoadingSpinner></LoadingSpinner>}
</div>}
</CardContent>
</Card >
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger>
<Button onClick={() => downloadDbGateFilesForAppAsync()} disabled={!isDbGateActive || loading}
variant="ghost"><Download /></Button>
</TooltipTrigger>
<TooltipContent>
<p>Download the "Files" folder from DB Gate.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>}
{loading && <LoadingSpinner></LoadingSpinner>}
</div>}
</>;
}

View File

@@ -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 <>
<Card>
<CardHeader>
<CardTitle>Database Access</CardTitle>
<CardDescription>Activate one of the following tools to access the database through your browser.</CardDescription>
</CardHeader>
<CardContent>
<DbGateDbTool app={app} />
</CardContent>
</Card >
</>;
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;