mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 21:49:19 -06:00
feat: created base class BaseDbToolService to share common code with other DB Tool Services
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>}
|
||||
</>;
|
||||
}
|
||||
|
||||
22
src/app/project/app/[appId]/credentials/db-tools.tsx
Normal file
22
src/app/project/app/[appId]/credentials/db-tools.tsx
Normal 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 >
|
||||
</>;
|
||||
}
|
||||
182
src/server/services/db-tool-services/base-db-tool.service.ts
Normal file
182
src/server/services/db-tool-services/base-db-tool.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/server/services/db-tool-services/dbgate.service.ts
Normal file
139
src/server/services/db-tool-services/dbgate.service.ts
Normal 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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user