From 4452fb373a62151ebae69d250f1ec1b75b9d2db8 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Mon, 18 Nov 2024 15:22:41 +0100 Subject: [PATCH] created init service for deployment --- Dockerfile | 3 + prisma/schema.prisma | 2 +- src/server.ts | 51 ++++++++--- src/server/services/init.service.ts | 135 ++++++++++++++++++++++++++++ src/server/utils/string.utils.ts | 2 +- 5 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 src/server/services/init.service.ts diff --git a/Dockerfile b/Dockerfile index f7a6ddb..8ab9525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,9 @@ RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public +RUN mkdir storage +RUN chown nextjs:nodejs storage + # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cc3ac1b..d4da184 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,7 +33,7 @@ generator zod { datasource db { provider = "sqlite" - url = "file:../db/data.db" + url = "file:../storage/db/data.db" } // *** The following code is for the default NextAuth.js schema diff --git a/src/server.ts b/src/server.ts index 45e859d..7d525be 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,28 +2,51 @@ import { createServer } from 'http' import { parse } from 'url' import next from 'next' import socketIoServer from './socket-io.server' +import initService from './server/services/init.service' +import { CommandExecutorUtils } from './server/utils/command-executor.utils' // Source: https://nextjs.org/docs/app/building-your-application/configuring/custom-server const port = parseInt(process.env.PORT || '3000', 10) const dev = process.env.NODE_ENV !== 'production' -const app = next({ dev }) -const handle = app.getRequestHandler() -app.prepare().then(() => { +async function setupQuickStack() { + console.log('Setting up QuickStack...'); + await initService.initializeQuickStack(); +} - const server = createServer((req, res) => { - const parsedUrl = parse(req.url!, true) - handle(req, res, parsedUrl) +async function initializeNextJs() { + + if (process.env.NODE_ENV === 'production') { + // update database + console.log('Running db migration...'); + await CommandExecutorUtils.runCommand('npx prisma migrate deploy'); + } + + const app = next({ dev }) + const handle = app.getRequestHandler() + + app.prepare().then(() => { + + const server = createServer((req, res) => { + const parsedUrl = parse(req.url!, true) + handle(req, res, parsedUrl) + }); + + socketIoServer.initialize(server); + server.listen(port) + + console.log( + `> Server listening at http://localhost:${port} as ${dev ? 'development' : process.env.NODE_ENV + }` + ) }); +} - socketIoServer.initialize(server); - server.listen(port) - - console.log( - `> Server listening at http://localhost:${port} as ${dev ? 'development' : process.env.NODE_ENV - }` - ) -}); +if (process.env.NODE_ENV === 'production' && process.env.START_MODE === 'setup') { + setupQuickStack(); +} else { + initializeNextJs(); +} diff --git a/src/server/services/init.service.ts b/src/server/services/init.service.ts new file mode 100644 index 0000000..19f16cf --- /dev/null +++ b/src/server/services/init.service.ts @@ -0,0 +1,135 @@ +import k3s from "../adapter/kubernetes-api.adapter"; +import { V1Deployment, V1Service } from "@kubernetes/client-node"; +import namespaceService from "./namespace.service"; +import { StringUtils } from "../utils/string.utils"; + +class InitService { + + private readonly QUICKSTACK_NAMESPACE = 'quickstack'; + private readonly QUICKSTACK_DEPLOYMENT_NAME = 'quickstack'; + + + async initializeQuickStack() { + await namespaceService.createNamespaceIfNotExists(this.QUICKSTACK_NAMESPACE) + await this.deleteExistingDeployment(); + await this.createOrUpdatePvc(); + await this.createDeployment(); + await this.createOrUpdateService(true); + console.log('QuickStack successfully initialized'); + } + + async createOrUpdateService(openNodePort = false) { + const serviceName = StringUtils.toServiceName(this.QUICKSTACK_DEPLOYMENT_NAME); + const body: V1Service = { + metadata: { + name: StringUtils.toServiceName(this.QUICKSTACK_DEPLOYMENT_NAME) + }, + spec: { + selector: { + app: this.QUICKSTACK_DEPLOYMENT_NAME + }, + ports: [{ + protocol: 'TCP', + port: 3000, + targetPort: 3000, + nodePort: openNodePort ? 3000 : undefined, + }] + } + }; + const existingService = await k3s.core.readNamespacedService(serviceName, this.QUICKSTACK_NAMESPACE); + if (existingService.body) { + console.warn('Service already exists, updating'); + return k3s.core.replaceNamespacedService(serviceName, this.QUICKSTACK_NAMESPACE, body); + } else { + console.warn('Service does not exist, creating'); + return k3s.core.createNamespacedService(this.QUICKSTACK_NAMESPACE, body); + } + } + + + private async createOrUpdatePvc() { + const pvcName = StringUtils.toPvcName(this.QUICKSTACK_DEPLOYMENT_NAME); + const pvc = { + apiVersion: 'v1', + kind: 'PersistentVolumeClaim', + metadata: { + name: pvcName, + namespace: this.QUICKSTACK_NAMESPACE + }, + spec: { + accessModes: ['ReadWriteOnce'], + storageClassName: 'longhorn', + resources: { + requests: { + storage: '1Gi' + } + } + } + }; + const existingPvc = await k3s.core.readNamespacedPersistentVolumeClaim(pvcName, this.QUICKSTACK_NAMESPACE); + if (existingPvc.body) { + console.warn('PVC already exists, updating'); + await k3s.core.replaceNamespacedPersistentVolumeClaim(pvcName, this.QUICKSTACK_NAMESPACE, pvc); + } else { + console.warn('PVC does not exist, creating'); + await k3s.core.createNamespacedPersistentVolumeClaim(this.QUICKSTACK_NAMESPACE, pvc); + } + } + + + private async createDeployment() { + const body: V1Deployment = { + metadata: { + name: this.QUICKSTACK_DEPLOYMENT_NAME, + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: this.QUICKSTACK_DEPLOYMENT_NAME + } + }, + template: { + metadata: { + labels: { + app: this.QUICKSTACK_DEPLOYMENT_NAME + } + }, + spec: { + containers: [ + { + name: this.QUICKSTACK_DEPLOYMENT_NAME, + image: 'quickstack/quickstack:latest', + imagePullPolicy: 'Always', + volumeMounts: [{ + name: 'quickstack-volume', + mountPath: '/app/storage' + }] + } + ], + volumes: [{ + name: 'quickstack-volume', + persistentVolumeClaim: { + claimName: StringUtils.toPvcName(this.QUICKSTACK_DEPLOYMENT_NAME) + } + }] + } + } + } + }; + await k3s.apps.createNamespacedDeployment(this.QUICKSTACK_NAMESPACE, body); + console.log('Deployment created'); + } + + private async deleteExistingDeployment() { + const existingDeployments = await k3s.apps.readNamespacedDeployment(this.QUICKSTACK_DEPLOYMENT_NAME, this.QUICKSTACK_NAMESPACE); + const quickStackAlreadyDeployed = !!existingDeployments.body; + if (quickStackAlreadyDeployed) { + console.warn('QuickStack already deployed, deleting existing deployment (data wont be lost)'); + await k3s.apps.deleteNamespacedDeployment(this.QUICKSTACK_DEPLOYMENT_NAME, this.QUICKSTACK_NAMESPACE); + } + } +} + +const initService = new InitService(); +export default initService; diff --git a/src/server/utils/string.utils.ts b/src/server/utils/string.utils.ts index b935fc1..4df1fd4 100644 --- a/src/server/utils/string.utils.ts +++ b/src/server/utils/string.utils.ts @@ -49,7 +49,7 @@ export class StringUtils { return `svc-${appId}`; } - static toPvcName(volumeId: string) { + static toPvcName(volumeId: string): `pvc-${string}` { return `pvc-${volumeId}`; }