mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-17 18:28:48 -06:00
added cluster issuer configuration
This commit is contained in:
44
setup.sh
44
setup.sh
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -38,6 +38,4 @@ export const updateLetsEncryptSettings = async (prevState: any, inputData: QsLet
|
||||
});
|
||||
|
||||
await quickStackService.createOrUpdateCertIssuer(validatedData.letsEncryptMail);
|
||||
// todo update or deploy the cert issuer
|
||||
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user