added cluster issuer configuration

This commit is contained in:
biersoeckli
2024-11-22 11:50:01 +00:00
parent dc6e80834a
commit aaaeb0f429
9 changed files with 97 additions and 84 deletions

View File

@@ -61,50 +61,6 @@ echo "Waiting for Cert-Manager to start..."
wait_until_all_pods_running
sudo kubectl -n cert-manager get pod
# add Cluster Issuer
cat <<EOF > cluster-issuer.yaml
# Staging ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: default
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: test@ost.ch
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- selector: {}
http01:
ingress:
class: traefik
---
# Production ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
namespace: default
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: test@ost.ch
privateKeySecretRef:
name: letsencrypt-production
solvers:
- selector: {}
http01:
ingress:
class: traefik
EOF
sudo kubectl apply -f cluster-issuer.yaml
sudo kubectl get clusterissuer -o wide
rm cluster-issuer.yaml
sudo kubectl get nodes
# deploy QuickStack
cat <<EOF > quickstack-setup-job.yaml
apiVersion: v1

View File

@@ -1,20 +1,30 @@
'use server'
import { AuthFormInputSchema, authFormInputSchemaZod } from "@/model/auth-form";
import { AuthFormInputSchema, authFormInputSchemaZod, RegisterFormInputSchema, registgerFormInputSchemaZod } from "@/model/auth-form";
import { SuccessActionResult } from "@/model/server-action-error-return.model";
import { ServiceException } from "@/model/service.exception.model";
import quickStackService from "@/server/services/qs.service";
import userService from "@/server/services/user.service";
import { saveFormAction } from "@/server/utils/action-wrapper.utils";
export const registerUser = async (prevState: any, inputData: AuthFormInputSchema) =>
saveFormAction(inputData, authFormInputSchemaZod, async (validatedData) => {
export const registerUser = async (prevState: any, inputData: RegisterFormInputSchema) =>
saveFormAction(inputData, registgerFormInputSchemaZod, async (validatedData) => {
const allUsers = await userService.getAllUsers();
if (allUsers.length !== 0) {
throw new ServiceException("User registration is currently not possible");
}
return await userService.registerUser(validatedData.email, validatedData.password);
await userService.registerUser(validatedData.email, validatedData.password);
await quickStackService.createOrUpdateCertIssuer(validatedData.email);
if (validatedData.qsHostname) {
const url = new URL(validatedData.qsHostname.includes('://') ? validatedData.qsHostname : `https://${validatedData.qsHostname}`);
await quickStackService.createOrUpdateIngress(url.hostname);
return new SuccessActionResult(undefined, 'QuickStack is now available at: ' + url.href);
}
return new SuccessActionResult(undefined, 'Successfully registered user');
});
export const authUser = async (inputData: AuthFormInputSchema) =>
saveFormAction(inputData, authFormInputSchemaZod, async (validatedData) => {
const authResult = await userService.authorize({

View File

@@ -52,7 +52,7 @@ export default function UserLoginForm() {
setAuthInput(data); // 2fa window will be shown
}
} catch (e) {
console.log(e);
console.error(e);
setErrorMessages((e as any).message);
} finally {
setLoading(false);

View File

@@ -3,6 +3,7 @@
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -15,28 +16,32 @@ import { useFormState } from 'react-dom'
import { useEffect } from "react";
import { FormUtils } from "@/lib/form.utilts";
import { SubmitButton } from "@/components/custom/submit-button";
import { AuthFormInputSchema, authFormInputSchemaZod } from "@/model/auth-form"
import { AuthFormInputSchema, authFormInputSchemaZod, RegisterFormInputSchema, registgerFormInputSchemaZod } from "@/model/auth-form"
import { registerUser } from "./actions"
import { signIn } from "next-auth/react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { redirect } from "next/navigation"
import FormLabelWithQuestion from "@/components/custom/form-label-with-question"
import { toast } from "sonner"
export default function UserRegistrationForm() {
const form = useForm<AuthFormInputSchema>({
resolver: zodResolver(authFormInputSchemaZod)
const form = useForm<RegisterFormInputSchema>({
resolver: zodResolver(registgerFormInputSchemaZod)
});
const [state, formAction] = useFormState(registerUser, FormUtils.getInitialFormState<typeof authFormInputSchemaZod>());
const [state, formAction] = useFormState(registerUser, FormUtils.getInitialFormState<typeof registgerFormInputSchemaZod>());
useEffect(() => {
if (state.status === 'success') {
toast.success(state.message ?? 'Registration successful. You can now login.');
const formValues = form.getValues();
signIn("credentials", {
username: formValues.email,
password: formValues.password,
redirect: false,
}).then(() => {
redirect('/');
});
redirect('/');
}
}, [state]);
@@ -77,10 +82,25 @@ export default function UserRegistrationForm() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="qsHostname"
render={({ field }) => (
<FormItem>
<FormLabelWithQuestion hint="This domain will be used to access your QuickStack instance. Make sure the DNS settings of the domain are correctly configured to point to the server IP address. This can also be configured later in the QuickStack settings.">
QuickStack Domain (optional)
</FormLabelWithQuestion>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<p className="text-red-500">{state?.message}</p>
</CardContent>
<CardFooter>
<SubmitButton className="w-full">Register</SubmitButton>
</CardFooter>
</form>

View File

@@ -38,6 +38,4 @@ export const updateLetsEncryptSettings = async (prevState: any, inputData: QsLet
});
await quickStackService.createOrUpdateCertIssuer(validatedData.letsEncryptMail);
// todo update or deploy the cert issuer
});

View File

@@ -4,12 +4,15 @@ export const authFormInputSchemaZod = z.object({
email: z.string().email(),
password: z.string().min(1)
});
export type AuthFormInputSchema = z.infer<typeof authFormInputSchemaZod>;
export const registgerFormInputSchemaZod = authFormInputSchemaZod.merge(z.object({
qsHostname: z.string().url().optional(),
}));
export type RegisterFormInputSchema = z.infer<typeof registgerFormInputSchemaZod>;
export const twoFaInputSchemaZod = z.object({
twoFactorCode: z.string().length(6)
});
export type TwoFaInputSchema = z.infer<typeof twoFaInputSchemaZod>;

View File

@@ -71,6 +71,7 @@ declare const globalThis: {
*/
class K3sApiAdapter {
core: k8s.CoreV1Api;
apps: k8s.AppsV1Api;
batch: k8s.BatchV1Api;
@@ -78,7 +79,6 @@ class K3sApiAdapter {
network: k8s.NetworkingV1Api;
customObjects: k8s.CustomObjectsApi;
constructor() {
this.core = this.getK8sCoreApiClient();
this.apps = this.getK8sAppsApiClient();
@@ -116,7 +116,6 @@ class K3sApiAdapter {
return k8sJobClient;
}
getK8sLogApiClient = () => {
const kc = this.getKubeConfig()
const logClient = new k8s.Log(kc)

View File

@@ -123,7 +123,6 @@ class IngressService {
}
}
async checkIfTraefikRedirectMiddlewareExists() {
const res = await k3s.customObjects.listNamespacedCustomObject(
'traefik.io', // group
@@ -144,7 +143,7 @@ class IngressService {
kind: 'Middleware',
metadata: {
name: 'redirect-to-https',
traefikNamespace,
namespace: traefikNamespace,
},
spec: {
redirectScheme: {
@@ -161,7 +160,6 @@ class IngressService {
'middlewares', // plural name of the custom resource
middlewareManifest // object manifest
);
}
}

View File

@@ -3,8 +3,6 @@ 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 QuickStackService {
@@ -12,6 +10,7 @@ class QuickStackService {
private readonly QUICKSTACK_DEPLOYMENT_NAME = 'quickstack';
private readonly QUICKSTACK_PORT_NUMBER = 3000;
private readonly QUICKSTACK_SERVICEACCOUNT_NAME = 'qs-service-account';
private readonly CLUSTER_ISSUER_NAME = 'letsencrypt-production';
async initializeQuickStack() {
@@ -35,7 +34,7 @@ class QuickStackService {
name: ingressName,
namespace: this.QUICKSTACK_NAMESPACE,
annotations: {
'cert-manager.io/cluster-issuer': 'letsencrypt-production',
'cert-manager.io/cluster-issuer': this.CLUSTER_ISSUER_NAME,
'traefik.ingress.kubernetes.io/router.middlewares': 'kube-system-redirect-to-https@kubernetescrd' // activate redirect middleware for https
},
},
@@ -81,26 +80,28 @@ class QuickStackService {
}
async createOrUpdateCertIssuer(letsencryptMail: string) {
const issuerName = 'letsencrypt-production';
const issuerDefinition = {
const now = new Date();
const clusterIssuerBody = {
apiVersion: 'cert-manager.io/v1',
kind: 'ClusterIssuer',
metadata: {
name: issuerName,
namespace: 'default'
name: this.CLUSTER_ISSUER_NAME,
namespace: 'default',
//resourceVersion: now.getTime().toString(),
},
spec: {
acme: {
email: letsencryptMail,
server: 'https://acme-v02.api.letsencrypt.org/directory',
privateKeySecretRef: {
name: 'letsencrypt-production'
name: this.CLUSTER_ISSUER_NAME,
},
solvers: [
{
selector: {},
http01: {
ingress: {
class: 'traefik'
class: "traefik"
}
}
}
@@ -108,15 +109,45 @@ class QuickStackService {
}
}
};
// 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');*/
if (await this.checkIfClusterIssuerExists()) {
// update
await k3s.customObjects.patchClusterCustomObject(
'cert-manager.io', // group
'v1', // version
'clusterissuers', // plural name of the custom resource
this.CLUSTER_ISSUER_NAME, // name of the custom resource
clusterIssuerBody, // object manifest
undefined, undefined, undefined, {
headers: { 'Content-Type': 'application/merge-patch+json' },
}
);
} else {
// create
await k3s.customObjects.createClusterCustomObject(
'cert-manager.io', // group
'v1', // version
'clusterissuers', // plural name of the custom resource
clusterIssuerBody // object manifest
);
}
}
async checkIfClusterIssuerExists() {
const res = await k3s.customObjects.listClusterCustomObject(
'cert-manager.io', // group
'v1', // namespace
'clusterissuers', // plural name of the custom resource
);
if ((res.body as any) && (res.body as any)?.items && (res.body as any)?.items?.length > 0) {
const existingLetsecryptProduction = (res.body as any).items.find((item: any) => item.metadata.name === this.CLUSTER_ISSUER_NAME);
if (existingLetsecryptProduction) {
return true;
}
}
return false;
}
async createOrUpdateService(openNodePort = false) {
@@ -157,7 +188,6 @@ class QuickStackService {
console.log('Service created');
}
private async createOrUpdatePvc() {
const pvcName = StringUtils.toPvcName(this.QUICKSTACK_DEPLOYMENT_NAME);
const pvc = {
@@ -197,7 +227,6 @@ class QuickStackService {
}
}
async createOrUpdateDeployment(nextAuthHostname?: string, inputNextAuthSecret?: string) {
const generatedNextAuthSecret = crypto.randomBytes(32).toString('base64');
const existingDeployment = await this.getExistingDeployment();