mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 01:01:43 -06:00
added settings page for letsencrypt and ingress configuration for quickstack
This commit is contained in:
7
prisma/migrations/20241121151959_migration/migration.sql
Normal file
7
prisma/migrations/20241121151959_migration/migration.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Parameter" (
|
||||
"name" TEXT NOT NULL PRIMARY KEY,
|
||||
"value" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
@@ -178,3 +178,11 @@ model AppVolume {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Parameter {
|
||||
name String @id
|
||||
value String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
1
setup.sh
1
setup.sh
@@ -137,6 +137,7 @@ metadata:
|
||||
name: quickstack-setup-job
|
||||
namespace: quickstack
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 3600
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: qs-service-account
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { SimpleDataTable } from "@/components/custom/simple-data-table";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import LoadingSpinner from "@/components/ui/loading-spinner";
|
||||
import { formatDateTime } from "@/lib/format.utils";
|
||||
import { AppExtendedModel } from "@/model/app-extended.model";
|
||||
import { BuildJobModel } from "@/model/build-job";
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteBuild, getDeploymentsAndBuildsForApp } from "./actions";
|
||||
import { set } from "date-fns";
|
||||
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
|
||||
import { Item } from "@radix-ui/react-dropdown-menu";
|
||||
import BuildStatusBadge from "./build-status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useConfirmDialog } from "@/lib/zustand.states";
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { DeploymentInfoModel } from "@/model/deployment-info.model";
|
||||
import DeploymentStatusBadge from "./deployment-status-badge";
|
||||
import { io } from "socket.io-client";
|
||||
import { podLogsSocket } from "@/lib/sockets";
|
||||
import { BuildLogsDialog } from "./build-logs-overlay";
|
||||
import ShortCommitHash from "@/components/custom/short-commit-hash";
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
'use server'
|
||||
|
||||
import { ServiceException } from "@/model/service.exception.model";
|
||||
import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model";
|
||||
import userService from "@/server/services/user.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { TotpModel, totpZodModel } from "@/model/update-password.model copy";
|
||||
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { SuccessActionResult } from "@/model/server-action-error-return.model";
|
||||
import clusterService from "@/server/services/node.service";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ServiceException } from "@/model/service.exception.model";
|
||||
import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model";
|
||||
import userService from "@/server/services/user.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { TotpModel, totpZodModel } from "@/model/update-password.model copy";
|
||||
import { TotpModel, totpZodModel } from "@/model/totp.model";
|
||||
import { SuccessActionResult } from "@/model/server-action-error-return.model";
|
||||
|
||||
export const changePassword = async (prevState: any, inputData: ProfilePasswordChangeModel) =>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { toast } from "sonner";
|
||||
import { createNewTotpToken, verifyTotpToken } from "./actions";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import React from "react";
|
||||
import { TotpModel, totpZodModel } from "@/model/update-password.model copy";
|
||||
import { TotpModel, totpZodModel } from "@/model/totp.model";
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
|
||||
|
||||
|
||||
39
src/app/settings/server/actions.ts
Normal file
39
src/app/settings/server/actions.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
'use server'
|
||||
|
||||
import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import { QsIngressSettingsModel, qsIngressSettingsZodModel } from "@/model/qs-settings.model";
|
||||
import { QsLetsEncryptSettingsModel, qsLetsEncryptSettingsZodModel } from "@/model/qs-letsencrypt-settings.model";
|
||||
import quickStackService from "@/server/services/qs.service";
|
||||
|
||||
export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
|
||||
saveFormAction(inputData, qsIngressSettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
|
||||
await paramService.save({
|
||||
name: ParamService.QS_SERVER_HOSTNAME,
|
||||
value: validatedData.serverUrl
|
||||
});
|
||||
|
||||
await paramService.save({
|
||||
name: ParamService.DISABLE_NODEPORT_ACCESS,
|
||||
value: validatedData.disableNodePortAccess + ''
|
||||
});
|
||||
|
||||
await quickStackService.createOrUpdateService(!validatedData.disableNodePortAccess);
|
||||
await quickStackService.createOrUpdateIngress(validatedData.serverUrl);
|
||||
});
|
||||
|
||||
export const updateLetsEncryptSettings = async (prevState: any, inputData: QsLetsEncryptSettingsModel) =>
|
||||
saveFormAction(inputData, qsLetsEncryptSettingsZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
|
||||
await paramService.save({
|
||||
name: ParamService.LETS_ENCRYPT_MAIL,
|
||||
value: validatedData.letsEncryptMail
|
||||
});
|
||||
|
||||
await quickStackService.createOrUpdateCertIssuer(validatedData.letsEncryptMail);
|
||||
// todo update or deploy the cert issuer
|
||||
|
||||
});
|
||||
@@ -9,17 +9,26 @@ import {
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import PageTitle from "@/components/custom/page-title";
|
||||
import userService from "@/server/services/user.service";
|
||||
import paramService, { ParamService } from "@/server/services/param.service";
|
||||
import QuickStackIngressSettings from "./qs-ingress-settings";
|
||||
import QuickStackLetsEncryptSettings from "./qs-letsencrypt-settings";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
const session = await getAuthUserSession();
|
||||
const data = await userService.getUserByEmail(session.email);
|
||||
const serverUrl = await paramService.getString(ParamService.QS_SERVER_HOSTNAME, '');
|
||||
const disableNodePortAccess = await paramService.getBoolean(ParamService.DISABLE_NODEPORT_ACCESS, false);
|
||||
const letsEncryptMail = await paramService.getString(ParamService.LETS_ENCRYPT_MAIL, session.email);
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<PageTitle
|
||||
title={'Server Settings'}
|
||||
subtitle={`View or edit Server Settings`}>
|
||||
</PageTitle>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div><QuickStackIngressSettings disableNodePortAccess={disableNodePortAccess!} serverUrl={serverUrl!} /></div>
|
||||
<div> <QuickStackLetsEncryptSettings letsEncryptMail={letsEncryptMail!} /></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
91
src/app/settings/server/qs-ingress-settings.tsx
Normal file
91
src/app/settings/server/qs-ingress-settings.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { FormUtils } from "@/lib/form.utilts";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFormState } from "react-dom";
|
||||
import { ServerActionResult } from "@/model/server-action-error-return.model";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { QsIngressSettingsModel, qsIngressSettingsZodModel } from "@/model/qs-settings.model";
|
||||
import { updateIngressSettings } from "./actions";
|
||||
import CheckboxFormField from "@/components/custom/checkbox-form-field";
|
||||
|
||||
export default function QuickStackIngressSettings({
|
||||
serverUrl,
|
||||
disableNodePortAccess
|
||||
}: {
|
||||
serverUrl: string;
|
||||
disableNodePortAccess: boolean;
|
||||
}) {
|
||||
const form = useForm<QsIngressSettingsModel>({
|
||||
resolver: zodResolver(qsIngressSettingsZodModel),
|
||||
defaultValues: {
|
||||
serverUrl,
|
||||
disableNodePortAccess: !disableNodePortAccess
|
||||
}
|
||||
});
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: QsIngressSettingsModel) =>
|
||||
updateIngressSettings(state, payload), FormUtils.getInitialFormState<typeof qsIngressSettingsZodModel>());
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
toast.success('Settings updated successfully. It may take a few seconds for the changes to take effect.');
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof qsIngressSettingsZodModel>(state, form)
|
||||
}, [state]);
|
||||
|
||||
const sourceTypeField = form.watch();
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Panel Domain</CardTitle>
|
||||
<CardDescription>Change the domain settings for your QuickStack instance.</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction({
|
||||
...data,
|
||||
disableNodePortAccess: !data.disableNodePortAccess
|
||||
});
|
||||
})()}>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Domain</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Make sure the DNS settings of the domain are correctly configured to point to the server IP address.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxFormField
|
||||
form={form}
|
||||
name="disableNodePortAccess"
|
||||
label="Serve QuickStack over IP Address and Port 30000"
|
||||
/>
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="gap-4">
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
<p className="text-red-500">{state?.message}</p>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Form >
|
||||
</Card >
|
||||
|
||||
</>;
|
||||
}
|
||||
79
src/app/settings/server/qs-letsencrypt-settings.tsx
Normal file
79
src/app/settings/server/qs-letsencrypt-settings.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { FormUtils } from "@/lib/form.utilts";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFormState } from "react-dom";
|
||||
import { ServerActionResult } from "@/model/server-action-error-return.model";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ProfilePasswordChangeModel, profilePasswordChangeZodModel } from "@/model/update-password.model";
|
||||
import { QsIngressSettingsModel, qsIngressSettingsZodModel } from "@/model/qs-settings.model";
|
||||
import { updateIngressSettings, updateLetsEncryptSettings } from "./actions";
|
||||
import SelectFormField from "@/components/custom/select-form-field";
|
||||
import CheckboxFormField from "@/components/custom/checkbox-form-field";
|
||||
import { QsLetsEncryptSettingsModel, qsLetsEncryptSettingsZodModel } from "@/model/qs-letsencrypt-settings.model";
|
||||
|
||||
export default function QuickStackLetsEncryptSettings({
|
||||
letsEncryptMail,
|
||||
}: {
|
||||
letsEncryptMail: string;
|
||||
}) {
|
||||
const form = useForm<QsLetsEncryptSettingsModel>({
|
||||
resolver: zodResolver(qsLetsEncryptSettingsZodModel),
|
||||
defaultValues: {
|
||||
letsEncryptMail,
|
||||
}
|
||||
});
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: QsLetsEncryptSettingsModel) =>
|
||||
updateLetsEncryptSettings(state, payload), FormUtils.getInitialFormState<typeof qsLetsEncryptSettingsZodModel>());
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
toast.success('Settings updated successfully. It may take a few seconds for the changes to take effect.');
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof qsLetsEncryptSettingsZodModel>(state, form)
|
||||
}, [state]);
|
||||
|
||||
const sourceTypeField = form.watch();
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SSL Certificates</CardTitle>
|
||||
<CardDescription>To issue SSL Certificates to your Apps, provide your Let's Encrypt email address.</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="letsEncryptMail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Let's Encrypt Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="gap-4">
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
<p className="text-red-500">{state?.message}</p>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Form >
|
||||
</Card >
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from "./project"
|
||||
export * from "./app"
|
||||
export * from "./appdomain"
|
||||
export * from "./appvolume"
|
||||
export * from "./parameter"
|
||||
|
||||
9
src/model/generated-zod/parameter.ts
Normal file
9
src/model/generated-zod/parameter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as z from "zod"
|
||||
|
||||
|
||||
export const ParameterModel = z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
8
src/model/qs-letsencrypt-settings.model.ts
Normal file
8
src/model/qs-letsencrypt-settings.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { stringToBoolean } from "@/lib/zod.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const qsLetsEncryptSettingsZodModel = z.object({
|
||||
letsEncryptMail: z.string().trim().email(),
|
||||
})
|
||||
|
||||
export type QsLetsEncryptSettingsModel = z.infer<typeof qsLetsEncryptSettingsZodModel>;
|
||||
9
src/model/qs-settings.model.ts
Normal file
9
src/model/qs-settings.model.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { stringToBoolean } from "@/lib/zod.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const qsIngressSettingsZodModel = z.object({
|
||||
serverUrl: z.string().trim().min(1),
|
||||
disableNodePortAccess: stringToBoolean,
|
||||
})
|
||||
|
||||
export type QsIngressSettingsModel = z.infer<typeof qsIngressSettingsZodModel>;
|
||||
@@ -2,7 +2,7 @@ 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 quickStackService from './server/services/qs.service'
|
||||
import { CommandExecutorUtils } from './server/utils/command-executor.utils'
|
||||
import k3s from './server/adapter/kubernetes-api.adapter'
|
||||
|
||||
@@ -19,7 +19,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
async function setupQuickStack() {
|
||||
console.log('Setting up QuickStack...');
|
||||
await initService.initializeQuickStack();
|
||||
await quickStackService.initializeQuickStack();
|
||||
}
|
||||
|
||||
async function initializeNextJs() {
|
||||
|
||||
@@ -15,12 +15,11 @@ class IngressService {
|
||||
return res.body.items.filter((item) => item.metadata?.annotations?.[Constants.QS_ANNOTATION_APP_ID] === appId);
|
||||
}
|
||||
|
||||
async getIngress(projectId: string, domainId: string, ) {
|
||||
async getIngress(projectId: string, domainId: string) {
|
||||
const res = await k3s.network.listNamespacedIngress(projectId);
|
||||
return res.body.items.find((item) => item.metadata?.name === StringUtils.getIngressName(domainId));
|
||||
}
|
||||
|
||||
|
||||
async deleteUnusedIngressesOfApp(app: AppExtendedModel) {
|
||||
const currentDomains = new Set(app.appDomains.map(domainObj => domainObj.hostname));
|
||||
const existingIngresses = await this.getAllIngressForApp(app.projectId, app.id);
|
||||
|
||||
130
src/server/services/param.service.ts
Normal file
130
src/server/services/param.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { Parameter, Prisma } from "@prisma/client";
|
||||
|
||||
export class ParamService {
|
||||
|
||||
static readonly QS_SERVER_HOSTNAME = 'qsServerHostname';
|
||||
static readonly DISABLE_NODEPORT_ACCESS = 'disableNodePortAccess';
|
||||
static readonly LETS_ENCRYPT_MAIL = 'letsEncryptMail';
|
||||
|
||||
async get(name: string) {
|
||||
return await unstable_cache(async (name: string) => await dataAccess.client.parameter.findFirstOrThrow({
|
||||
where: {
|
||||
name
|
||||
}
|
||||
}),
|
||||
[Tags.parameter()], {
|
||||
tags: [Tags.parameter()]
|
||||
})(name);
|
||||
}
|
||||
|
||||
async getOrUndefined(name: string) {
|
||||
return await unstable_cache(async (name: string) => await dataAccess.client.parameter.findUnique({
|
||||
where: {
|
||||
name
|
||||
}
|
||||
}),
|
||||
[Tags.parameter()], {
|
||||
tags: [Tags.parameter()]
|
||||
})(name);
|
||||
}
|
||||
|
||||
async getBoolean(name: string, defaultValue?: boolean) {
|
||||
const param = await this.getOrUndefined(name);
|
||||
if (param) {
|
||||
return param.value === 'true';
|
||||
}
|
||||
if (defaultValue) {
|
||||
await this.save({
|
||||
name,
|
||||
value: defaultValue.toString()
|
||||
});
|
||||
return defaultValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getString(name: string, defaultValue?: string) {
|
||||
const param = await this.getOrUndefined(name);
|
||||
if (param) {
|
||||
return param.value;
|
||||
}
|
||||
if (defaultValue) {
|
||||
await this.save({
|
||||
name,
|
||||
value: defaultValue
|
||||
});
|
||||
return defaultValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getNumber(name: string, defaultValue?: number) {
|
||||
const param = await this.getOrUndefined(name);
|
||||
if (param) {
|
||||
return Number(param.value);
|
||||
}
|
||||
if (defaultValue) {
|
||||
await this.save({
|
||||
name,
|
||||
value: defaultValue.toString()
|
||||
});
|
||||
return defaultValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async deleteByName(name: string) {
|
||||
const existingParam = await this.get(name);
|
||||
if (!existingParam) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await dataAccess.client.parameter.delete({
|
||||
where: {
|
||||
name
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
revalidateTag(Tags.parameter());
|
||||
}
|
||||
}
|
||||
|
||||
async getAllParams() {
|
||||
return await unstable_cache(async () => await dataAccess.client.parameter.findMany(),
|
||||
[Tags.parameter()], {
|
||||
tags: [Tags.parameter()]
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
async save(item: Prisma.ParameterUncheckedCreateInput | Prisma.ParameterUncheckedUpdateInput) {
|
||||
let savedItem: Parameter;
|
||||
try {
|
||||
const existingParam = await this.getOrUndefined(item.name as string);
|
||||
if (existingParam) {
|
||||
savedItem = await dataAccess.client.parameter.update({
|
||||
where: {
|
||||
name: item.name as string
|
||||
},
|
||||
data: {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
savedItem = await dataAccess.client.parameter.create({
|
||||
data: item as Prisma.ParameterUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
revalidateTag(Tags.parameter());
|
||||
}
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const paramService = new ParamService();
|
||||
export default paramService;
|
||||
@@ -1,13 +1,16 @@
|
||||
import k3s from "../adapter/kubernetes-api.adapter";
|
||||
import { V1Deployment, V1Service } from "@kubernetes/client-node";
|
||||
import { V1Deployment, V1Ingress, V1Service } from "@kubernetes/client-node";
|
||||
import namespaceService from "./namespace.service";
|
||||
import { StringUtils } from "../utils/string.utils";
|
||||
import crypto from "crypto";
|
||||
import paramService, { ParamService } from "./param.service";
|
||||
import { ServiceException } from "@/model/service.exception.model";
|
||||
|
||||
class InitService {
|
||||
class QuickStackService {
|
||||
|
||||
private readonly QUICKSTACK_NAMESPACE = 'quickstack';
|
||||
private readonly QUICKSTACK_DEPLOYMENT_NAME = 'quickstack';
|
||||
private readonly QUICKSTACK_PORT_NUMBER = 3000;
|
||||
private readonly QUICKSTACK_SERVICEACCOUNT_NAME = 'qs-service-account';
|
||||
|
||||
|
||||
@@ -20,6 +23,102 @@ class InitService {
|
||||
console.log('QuickStack successfully initialized');
|
||||
}
|
||||
|
||||
async createOrUpdateIngress(hostname: string) {
|
||||
const ingressName = StringUtils.getIngressName(this.QUICKSTACK_NAMESPACE);
|
||||
const existingIngresses = await k3s.network.listNamespacedIngress(this.QUICKSTACK_NAMESPACE);
|
||||
const existingIngress = existingIngresses.body.items.find((item) => item.metadata?.name === ingressName);
|
||||
|
||||
const ingressDefinition: V1Ingress = {
|
||||
apiVersion: 'networking.k8s.io/v1',
|
||||
kind: 'Ingress',
|
||||
metadata: {
|
||||
name: ingressName,
|
||||
namespace: this.QUICKSTACK_NAMESPACE,
|
||||
annotations: {
|
||||
'cert-manager.io/cluster-issuer': 'letsencrypt-production',
|
||||
'traefik.ingress.kubernetes.io/router.middlewares': 'kube-system-redirect-to-https@kubernetescrd' // activate redirect middleware for https
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
ingressClassName: 'traefik',
|
||||
rules: [
|
||||
{
|
||||
host: hostname,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: {
|
||||
service: {
|
||||
name: StringUtils.toServiceName(this.QUICKSTACK_DEPLOYMENT_NAME),
|
||||
port: {
|
||||
number: this.QUICKSTACK_PORT_NUMBER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
tls: [
|
||||
{
|
||||
hosts: [hostname],
|
||||
secretName: `secret-tls-${hostname}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (existingIngress) {
|
||||
await k3s.network.replaceNamespacedIngress(ingressName, this.QUICKSTACK_NAMESPACE, ingressDefinition);
|
||||
console.log(`Ingress QuickStack for domain ${hostname} successfully updated.`);
|
||||
} else {
|
||||
await k3s.network.createNamespacedIngress(this.QUICKSTACK_NAMESPACE, ingressDefinition);
|
||||
console.log(`Ingress QuickStack for domain ${hostname} successfully created.`);
|
||||
}
|
||||
}
|
||||
|
||||
async createOrUpdateCertIssuer(letsencryptMail: string) {
|
||||
const issuerName = 'letsencrypt-production';
|
||||
const issuerDefinition = {
|
||||
apiVersion: 'cert-manager.io/v1',
|
||||
kind: 'ClusterIssuer',
|
||||
metadata: {
|
||||
name: issuerName,
|
||||
namespace: 'default'
|
||||
},
|
||||
spec: {
|
||||
acme: {
|
||||
email: letsencryptMail,
|
||||
server: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
privateKeySecretRef: {
|
||||
name: 'letsencrypt-production'
|
||||
},
|
||||
solvers: [
|
||||
{
|
||||
http01: {
|
||||
ingress: {
|
||||
class: 'traefik'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
// todo
|
||||
/* const allIssuers = await k3s.network.clus();
|
||||
const existingIssuer = allIssuers.body.items.find(i => i.metadata!.name === issuerName);
|
||||
if (existingIssuer) {
|
||||
await k3s.certManager.replaceClusterIssuer(issuerName, issuerDefinition);
|
||||
console.log('Cert Issuer updated');
|
||||
} else {
|
||||
await k3s.certManager.createClusterIssuer(issuerDefinition);
|
||||
console.log('Cert Issuer created');*/
|
||||
}
|
||||
|
||||
async createOrUpdateService(openNodePort = false) {
|
||||
const serviceName = StringUtils.toServiceName(this.QUICKSTACK_DEPLOYMENT_NAME);
|
||||
const body: V1Service = {
|
||||
@@ -36,8 +135,8 @@ class InitService {
|
||||
ports: [
|
||||
{
|
||||
protocol: 'TCP',
|
||||
port: 3000,
|
||||
targetPort: 3000,
|
||||
port: this.QUICKSTACK_PORT_NUMBER,
|
||||
targetPort: this.QUICKSTACK_PORT_NUMBER,
|
||||
nodePort: openNodePort ? 30000 : undefined,
|
||||
}
|
||||
],
|
||||
@@ -51,8 +150,6 @@ class InitService {
|
||||
console.warn('Service already exists, deleting and recreating it');
|
||||
await k3s.core.deleteNamespacedService(serviceName, this.QUICKSTACK_NAMESPACE);
|
||||
console.log('Existing service deleted');
|
||||
//await k3s.core.replaceNamespacedService(serviceName, this.QUICKSTACK_NAMESPACE, body);
|
||||
// console.log('Service created');
|
||||
} else {
|
||||
console.warn('Service does not exist, creating');
|
||||
}
|
||||
@@ -175,5 +272,5 @@ class InitService {
|
||||
}
|
||||
}
|
||||
|
||||
const initService = new InitService();
|
||||
export default initService;
|
||||
const quickStackService = new QuickStackService();
|
||||
export default quickStackService;
|
||||
@@ -12,6 +12,10 @@ export class Tags {
|
||||
return `apps-${projectId}`;
|
||||
}
|
||||
|
||||
static parameter() {
|
||||
return `parameter`;
|
||||
}
|
||||
|
||||
static app(appId: string) {
|
||||
return `app-${appId}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user