Enhancement: support for Kubernetes gateway API (#4643)

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Co-authored-by: lyons <gittea.sand@gmail.com>
Co-authored-by: Brett Dudo <brett@dudo.io>
This commit is contained in:
djeinstine
2025-02-12 03:57:22 +01:00
committed by GitHub
parent 2a95f88cdf
commit 91d5fc8e42
14 changed files with 407 additions and 168 deletions

View File

@@ -0,0 +1,14 @@
import listIngress from "utils/kubernetes/ingress-list";
import listTraefikIngress from "utils/kubernetes/traefik-list";
import listHttpRoute from "utils/kubernetes/httproute-list";
import { isDiscoverable, constructedServiceFromResource } from "utils/kubernetes/resource-helpers";
const kubernetes = {
listIngress,
listTraefikIngress,
listHttpRoute,
isDiscoverable,
constructedServiceFromResource,
};
export default kubernetes;

View File

@@ -0,0 +1,56 @@
import { CustomObjectsApi, CoreV1Api } from "@kubernetes/client-node";
import { getKubernetes, getKubeConfig, HTTPROUTE_API_GROUP, HTTPROUTE_API_VERSION } from "utils/config/kubernetes";
import createLogger from "utils/logger";
const logger = createLogger("httproute-list");
const kc = getKubeConfig();
export default async function listHttpRoute() {
const crd = kc.makeApiClient(CustomObjectsApi);
const core = kc.makeApiClient(CoreV1Api);
const { gateway } = getKubernetes();
let httpRouteList = [];
if (gateway) {
// httproutes
const getHttpRoute = async (namespace) =>
crd
.listNamespacedCustomObject({
group: HTTPROUTE_API_GROUP,
version: HTTPROUTE_API_VERSION,
namespace,
plural: "httproutes",
})
.then((response) => {
const [httpRoute] = response.items;
return httpRoute;
})
.catch((error) => {
logger.error("Error getting httproutes: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return null;
});
// namespaces
const namespaces = await core
.listNamespace()
.then((response) => response.items.map((ns) => ns.metadata.name))
.catch((error) => {
logger.error("Error getting namespaces: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return null;
});
if (namespaces) {
const httpRouteListUnfiltered = await Promise.all(
namespaces.map(async (namespace) => {
const httpRoute = await getHttpRoute(namespace);
return httpRoute;
}),
);
httpRouteList = httpRouteListUnfiltered.filter((httpRoute) => httpRoute !== undefined);
}
}
return httpRouteList;
}

View File

@@ -0,0 +1,26 @@
import { NetworkingV1Api } from "@kubernetes/client-node";
import { getKubernetes, getKubeConfig } from "utils/config/kubernetes";
import createLogger from "utils/logger";
const logger = createLogger("ingress-list");
const kc = getKubeConfig();
export default async function listIngress() {
const networking = kc.makeApiClient(NetworkingV1Api);
const { ingress } = getKubernetes();
let ingressList = [];
if (ingress) {
const ingressData = await networking
.listIngressForAllNamespaces()
.then((response) => response)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return null;
});
ingressList = ingressData.items;
}
return ingressList;
}

View File

@@ -0,0 +1,130 @@
import { CustomObjectsApi } from "@kubernetes/client-node";
import {
getKubeConfig,
ANNOTATION_BASE,
ANNOTATION_WIDGET_BASE,
HTTPROUTE_API_GROUP,
HTTPROUTE_API_VERSION,
} from "utils/config/kubernetes";
import { substituteEnvironmentVars } from "utils/config/config";
import createLogger from "utils/logger";
import * as shvl from "utils/config/shvl";
const logger = createLogger("resource-helpers");
const kc = getKubeConfig();
const getSchemaFromGateway = async (gatewayRef) => {
const crd = kc.makeApiClient(CustomObjectsApi);
const schema = await crd
.getNamespacedCustomObject({
group: HTTPROUTE_API_GROUP,
version: HTTPROUTE_API_VERSION,
namespace: gatewayRef.namespace,
plural: "gateways",
name: gatewayRef.name,
})
.then((response) => {
const listner = response.spec.listeners.filter((listener) => listener.name === gatewayRef.sectionName)[0];
return listner.protocol.toLowerCase();
})
.catch((error) => {
logger.error("Error getting gateways: %d %s %s", error.statusCode, error.body, error.response);
logger.debug(error);
return "";
});
return schema;
};
async function getUrlFromHttpRoute(resource) {
let url = null;
const hasHostName = resource.spec?.hostnames;
if (hasHostName) {
if (resource.spec.rules[0].matches[0].path.type !== "RegularExpression") {
const urlHost = resource.spec.hostnames[0];
const urlPath = resource.spec.rules[0].matches[0].path.value;
const urlSchema = (await getSchemaFromGateway(resource.spec.parentRefs[0])) ? "https" : "http";
url = `${urlSchema}://${urlHost}${urlPath}`;
}
}
return url;
}
function getUrlFromIngress(resource) {
const urlHost = resource.spec.rules[0].host;
const urlPath = resource.spec.rules[0].http.paths[0].path;
const urlSchema = resource.spec.tls ? "https" : "http";
return `${urlSchema}://${urlHost}${urlPath}`;
}
async function getUrlSchema(resource) {
const isHttpRoute = resource.kind === "HTTPRoute";
let urlSchema;
if (isHttpRoute) {
urlSchema = getUrlFromHttpRoute(resource);
} else {
urlSchema = getUrlFromIngress(resource);
}
return urlSchema;
}
export function isDiscoverable(resource, instanceName) {
return (
resource.metadata.annotations &&
resource.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
(!resource.metadata.annotations[`${ANNOTATION_BASE}/instance`] ||
resource.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName ||
`${ANNOTATION_BASE}/instance.${instanceName}` in resource.metadata.annotations)
);
}
export async function constructedServiceFromResource(resource) {
let constructedService = {
app: resource.metadata.annotations[`${ANNOTATION_BASE}/app`] || resource.metadata.name,
namespace: resource.metadata.namespace,
href: resource.metadata.annotations[`${ANNOTATION_BASE}/href`] || (await getUrlSchema(resource)),
name: resource.metadata.annotations[`${ANNOTATION_BASE}/name`] || resource.metadata.name,
group: resource.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
weight: resource.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
icon: resource.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
description: resource.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
external: false,
type: "service",
};
if (resource.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
constructedService.external =
String(resource.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
}
if (resource.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`] !== undefined) {
constructedService.podSelector = resource.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
}
if (resource.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
constructedService.ping = resource.metadata.annotations[`${ANNOTATION_BASE}/ping`];
}
if (resource.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`]) {
constructedService.siteMonitor = resource.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`];
}
if (resource.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
constructedService.statusStyle = resource.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
}
Object.keys(resource.metadata.annotations).forEach((annotation) => {
if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
shvl.set(
constructedService,
annotation.replace(`${ANNOTATION_BASE}/`, ""),
resource.metadata.annotations[annotation],
);
}
});
try {
constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
} catch (e) {
logger.error("Error attempting k8s environment variable substitution.");
logger.debug(e);
}
return constructedService;
}

View File

@@ -0,0 +1,70 @@
import { CustomObjectsApi } from "@kubernetes/client-node";
import { getKubernetes, getKubeConfig, checkCRD, ANNOTATION_BASE } from "utils/config/kubernetes";
import createLogger from "utils/logger";
const logger = createLogger("traefik-list");
const kc = getKubeConfig();
export default async function listTraefikIngress() {
const { traefik } = getKubernetes();
const traefikList = [];
if (traefik) {
const crd = kc.makeApiClient(CustomObjectsApi);
const traefikContainoExists = await checkCRD("ingressroutes.traefik.containo.us", kc, logger);
const traefikExists = await checkCRD("ingressroutes.traefik.io", kc, logger);
const traefikIngressListContaino = await crd
.listClusterCustomObject({
group: "traefik.containo.us",
version: "v1alpha1",
plural: "ingressroutes",
})
.then((response) => response)
.catch(async (error) => {
if (traefikContainoExists) {
logger.error(
"Error getting traefik ingresses from traefik.containo.us: %d %s %s",
error.statusCode,
error.body,
error.response,
);
logger.debug(error);
}
return [];
});
const traefikIngressListIo = await crd
.listClusterCustomObject({
group: "traefik.io",
version: "v1alpha1",
plural: "ingressroutes",
})
.then((response) => response.body)
.catch(async (error) => {
if (traefikExists) {
logger.error(
"Error getting traefik ingresses from traefik.io: %d %s %s",
error.statusCode,
error.body,
error.response,
);
logger.debug(error);
}
return [];
});
const traefikIngressList = [...(traefikIngressListContaino?.items ?? []), ...(traefikIngressListIo?.items ?? [])];
if (traefikIngressList.length > 0) {
const traefikServices = traefikIngressList.filter(
(ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`],
);
traefikList.push(...traefikServices);
}
}
return traefikList;
}