mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-05-19 00:08:32 -05:00
248 lines
11 KiB
TypeScript
248 lines
11 KiB
TypeScript
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
|
import k3s from "../adapter/kubernetes-api.adapter";
|
|
import { V1Ingress, V1Secret } from "@kubernetes/client-node";
|
|
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
|
import { Constants } from "../../shared/utils/constants";
|
|
import ingressSetupService from "./setup-services/ingress-setup.service";
|
|
import { dlog } from "./deployment-logs.service";
|
|
import { createHash } from "crypto";
|
|
|
|
class IngressService {
|
|
|
|
async getAllIngressForApp(projectId: string, appId: string) {
|
|
const res = await k3s.network.listNamespacedIngress(projectId);
|
|
return res.body.items.filter((item) => item.metadata?.annotations?.[Constants.QS_ANNOTATION_APP_ID] === appId);
|
|
}
|
|
|
|
async getIngressByName(projectId: string, domainId: string) {
|
|
const res = await k3s.network.listNamespacedIngress(projectId);
|
|
return res.body.items.find((item) => item.metadata?.name === KubeObjectNameUtils.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);
|
|
|
|
if (currentDomains.size === 0) {
|
|
for (const ingress of existingIngresses) {
|
|
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
|
|
console.log(`Deleted Ingress ${ingress.metadata!.name} for app ${app.id}`);
|
|
}
|
|
} else {
|
|
for (const ingress of existingIngresses) {
|
|
const ingressDomain = ingress.spec?.rules?.[0]?.host;
|
|
|
|
if (ingressDomain && !currentDomains.has(ingressDomain)) {
|
|
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, app.projectId);
|
|
console.log(`Deleted Ingress ${ingress.metadata!.name} for domain ${ingressDomain}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteAllIngressForApp(projectId: string, appId: string) {
|
|
const existingIngresses = await this.getAllIngressForApp(projectId, appId);
|
|
for (const ingress of existingIngresses) {
|
|
await k3s.network.deleteNamespacedIngress(ingress.metadata!.name!, projectId);
|
|
console.log(`Deleted Ingress ${ingress.metadata!.name} for app ${appId}`);
|
|
}
|
|
}
|
|
|
|
async createOrUpdateIngressForApp(deploymentId: string, app: AppExtendedModel) {
|
|
|
|
await ingressSetupService.createTraefikRedirectMiddlewareIfNotExist();
|
|
const basicAuthMiddlewareName = await this.configureBasicAuthForApp(app);
|
|
for (const domainObj of app.appDomains) {
|
|
await this.createOrUpdateIngress(deploymentId, app, domainObj, basicAuthMiddlewareName);
|
|
}
|
|
await this.deleteUnusedBasicAuthMiddlewaresForApp(app);
|
|
await this.deleteUnusedIngressesOfApp(app);
|
|
}
|
|
|
|
async createOrUpdateIngress(deploymentId: string,
|
|
app: { id: string, projectId: string },
|
|
domain: { id: string, hostname: string, port: number, useSsl: boolean, redirectHttps: boolean },
|
|
basicAuthMiddlewareName?: string) {
|
|
const hostname = domain.hostname;
|
|
const ingressName = KubeObjectNameUtils.getIngressName(domain.id);
|
|
const existingIngress = await this.getIngressByName(app.projectId, domain.id);
|
|
|
|
const middlewares = [
|
|
basicAuthMiddlewareName,
|
|
(domain.useSsl && domain.redirectHttps) ? 'kube-system-redirect-to-https@kubernetescrd' : undefined,
|
|
].filter((middleware) => !!middleware).join(',') ?? undefined;
|
|
|
|
const ingressDefinition: V1Ingress = {
|
|
apiVersion: 'networking.k8s.io/v1',
|
|
kind: 'Ingress',
|
|
metadata: {
|
|
name: ingressName,
|
|
namespace: app.projectId,
|
|
annotations: {
|
|
[Constants.QS_ANNOTATION_APP_ID]: app.id,
|
|
[Constants.QS_ANNOTATION_PROJECT_ID]: app.projectId,
|
|
...(domain.useSsl === true && { 'cert-manager.io/cluster-issuer': 'letsencrypt-production' }),
|
|
...(middlewares && { 'traefik.ingress.kubernetes.io/router.middlewares': middlewares }),
|
|
...(domain.useSsl === false && { 'traefik.ingress.kubernetes.io/router.entrypoints': 'web' }), // disable requests from https --> only http
|
|
},
|
|
},
|
|
spec: {
|
|
ingressClassName: 'traefik',
|
|
rules: [
|
|
{
|
|
host: hostname,
|
|
http: {
|
|
paths: [
|
|
{
|
|
path: '/',
|
|
pathType: 'Prefix',
|
|
backend: {
|
|
service: {
|
|
name: KubeObjectNameUtils.toServiceName(app.id),
|
|
port: {
|
|
number: domain.port,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
...(domain.useSsl === true && {
|
|
tls: [
|
|
{
|
|
hosts: [hostname],
|
|
secretName: `secret-tls-${domain.id}`,
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
};
|
|
|
|
await dlog(deploymentId, `Configuring Ingress with Domain ${domain.useSsl ? 'https' : 'http'}://${hostname} --> ${app.id}:${domain.port}`);
|
|
if (existingIngress) {
|
|
await k3s.network.replaceNamespacedIngress(ingressName, app.projectId, ingressDefinition);
|
|
console.log(`Ingress ${ingressName} for domain ${hostname} successfully updated.`);
|
|
} else {
|
|
await k3s.network.createNamespacedIngress(app.projectId, ingressDefinition);
|
|
console.log(`Ingress ${ingressName} for domain ${hostname} successfully created.`);
|
|
}
|
|
}
|
|
|
|
async configureBasicAuthForApp(app: AppExtendedModel) {
|
|
if (!app.appBasicAuths || app.appBasicAuths.length === 0) {
|
|
return undefined;
|
|
}
|
|
return await this.configureBasicAuthMiddleware(app.projectId, app.id, app.appBasicAuths.map(basicAuth => [basicAuth.username, basicAuth.password]));
|
|
}
|
|
|
|
async deleteUnusedBasicAuthMiddlewaresForApp(app: AppExtendedModel) {
|
|
if (!app.appBasicAuths || app.appBasicAuths.length > 0) {
|
|
return;
|
|
}
|
|
|
|
await this.deleteUnusedBasicAuthMiddlewares(app.projectId, app.id);
|
|
}
|
|
|
|
async deleteUnusedBasicAuthMiddlewares(namespace: string, basicAuthId: string) {
|
|
|
|
// delete middleware
|
|
const middlewareName = `ba-${basicAuthId}`;
|
|
const existingMiddlewares = await k3s.customObjects.listNamespacedCustomObject('traefik.io', // group
|
|
'v1alpha1', // version
|
|
namespace, // namespace
|
|
'middlewares' // plural name of the custom resource
|
|
);
|
|
const existingBasicAuthMiddleware = (existingMiddlewares.body as any).items.find((item: any) => item.metadata?.name === middlewareName);
|
|
if (existingBasicAuthMiddleware) {
|
|
await k3s.customObjects.deleteNamespacedCustomObject('traefik.io', 'v1alpha1', namespace, 'middlewares', middlewareName);
|
|
}
|
|
|
|
// delete secret
|
|
const secretName = `bas-${basicAuthId}`;
|
|
const existingSecrets = await k3s.core.listNamespacedSecret(namespace);
|
|
const existingSecret = existingSecrets.body.items.find((item) => item.metadata?.name === secretName);
|
|
if (existingSecret) {
|
|
await k3s.core.deleteNamespacedSecret(secretName, namespace);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures a basic auth middleware in a namespace.
|
|
* @returns middleware name for annotation in ingress controller
|
|
*/
|
|
async configureBasicAuthMiddleware(namespace: string, basicAuthId: string, usernamePassword: [string, string][]) {
|
|
|
|
const basicAuthNameMiddlewareName = `ba-${basicAuthId}`; // basic auth middleware
|
|
const basicAuthSecretName = `bas-${basicAuthId}`; // basic auth secret
|
|
|
|
const secretNamespace = namespace;
|
|
const middlewareNamespace = namespace;
|
|
|
|
// Create a secret with basic auth users
|
|
const existingSecrets = await k3s.core.listNamespacedSecret(secretNamespace);
|
|
const existingSecret = existingSecrets.body.items.find((item) => item.metadata?.name === basicAuthSecretName);
|
|
|
|
const usernameAndSha1PasswordStrings = usernamePassword.map(([username, password]) => `${username}:{SHA}${createHash('sha1').update(password).digest('base64')}`);
|
|
|
|
const secretManifest: V1Secret = {
|
|
apiVersion: 'v1',
|
|
kind: 'Secret',
|
|
metadata: {
|
|
name: basicAuthSecretName,
|
|
namespace: secretNamespace,
|
|
},
|
|
data: {
|
|
users: Buffer.from(usernameAndSha1PasswordStrings.join('\n')).toString('base64')
|
|
}
|
|
};
|
|
|
|
if (existingSecret) {
|
|
await k3s.core.deleteNamespacedSecret(basicAuthSecretName, secretNamespace);
|
|
}
|
|
await k3s.core.createNamespacedSecret(
|
|
secretNamespace, // namespace
|
|
secretManifest // object manifest
|
|
);
|
|
|
|
// Create a middleware with basic auth
|
|
const existingBasicAuthMiddlewares = await k3s.customObjects.listNamespacedCustomObject('traefik.io', // group
|
|
'v1alpha1', // version
|
|
middlewareNamespace, // namespace
|
|
'middlewares' // plural name of the custom resource
|
|
);
|
|
const existingBasicAuthMiddleware = (existingBasicAuthMiddlewares.body as any).items.find((item: any) => item.metadata?.name === basicAuthNameMiddlewareName);
|
|
|
|
const middlewareManifest = {
|
|
apiVersion: 'traefik.io/v1alpha1',
|
|
kind: 'Middleware',
|
|
metadata: {
|
|
name: basicAuthNameMiddlewareName,
|
|
namespace: middlewareNamespace,
|
|
},
|
|
spec: {
|
|
basicAuth: {
|
|
secret: basicAuthSecretName,
|
|
}
|
|
},
|
|
};
|
|
|
|
if (existingBasicAuthMiddleware) {
|
|
await k3s.customObjects.deleteNamespacedCustomObject('traefik.io', 'v1alpha1', middlewareNamespace, 'middlewares', basicAuthNameMiddlewareName);
|
|
}
|
|
await k3s.customObjects.createNamespacedCustomObject(
|
|
'traefik.io', // group
|
|
'v1alpha1', // version
|
|
middlewareNamespace, // namespace
|
|
'middlewares', // plural name of the custom resource
|
|
middlewareManifest // object manifest
|
|
);
|
|
|
|
return `${namespace}-${basicAuthNameMiddlewareName}@kubernetescrd`;
|
|
}
|
|
}
|
|
|
|
const ingressService = new IngressService();
|
|
export default ingressService;
|