mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
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:
parent
2a95f88cdf
commit
91d5fc8e42
@ -8,6 +8,7 @@ The Kubernetes connectivity has the following requirements:
|
|||||||
- Kubernetes 1.19+
|
- Kubernetes 1.19+
|
||||||
- Metrics Service
|
- Metrics Service
|
||||||
- An Ingress controller
|
- An Ingress controller
|
||||||
|
- Optionally: Gateway-API
|
||||||
|
|
||||||
The Kubernetes connection is configured in the `kubernetes.yaml` file. There are 3 modes to choose from:
|
The Kubernetes connection is configured in the `kubernetes.yaml` file. There are 3 modes to choose from:
|
||||||
|
|
||||||
@ -19,6 +20,22 @@ The Kubernetes connection is configured in the `kubernetes.yaml` file. There are
|
|||||||
mode: default
|
mode: default
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To configure Kubernetes gateway-api, ingress or ingressRoute service discovery, add one or multiple of the following settings.
|
||||||
|
|
||||||
|
Example settings:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress: true # enable ingress only
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ingress: true # enable ingress
|
||||||
|
traefik: true # enable traefik ingressRoute
|
||||||
|
gateway: true # enable gateway-api
|
||||||
|
```
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
Once the Kubernetes connection is configured, individual services can be configured to pull statistics. Only CPU and Memory are currently supported.
|
Once the Kubernetes connection is configured, individual services can be configured to pull statistics. Only CPU and Memory are currently supported.
|
||||||
@ -142,6 +159,10 @@ spec:
|
|||||||
|
|
||||||
If the `href` attribute is not present, Homepage will ignore the specific IngressRoute.
|
If the `href` attribute is not present, Homepage will ignore the specific IngressRoute.
|
||||||
|
|
||||||
|
### Gateway API HttpRoute support
|
||||||
|
|
||||||
|
Homepage also features automatic service discovery for Gateway API. Service definitions are read by annotating the HttpRoute custom resource definition and are indentical to the Ingress example as defined in [Automatic Service Discovery](#automatic-service-discovery).
|
||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
Similarly to Docker service discovery, there currently is no rigid ordering to discovered services and discovered services will be displayed above those specified in the `services.yaml`.
|
||||||
|
@ -216,6 +216,14 @@ rules:
|
|||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
- list
|
- list
|
||||||
|
- apiGroups:
|
||||||
|
- gateway.networking.k8s.io
|
||||||
|
resources:
|
||||||
|
- httproutes
|
||||||
|
- gateways
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- metrics.k8s.io
|
- metrics.k8s.io
|
||||||
resources:
|
resources:
|
||||||
|
@ -23,6 +23,12 @@ Set the `mode` in the `kubernetes.yaml` to `cluster`.
|
|||||||
mode: default
|
mode: default
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To enable Kubernetes gateway-api compatibility, set `route` to `gateway`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
route: gateway
|
||||||
|
```
|
||||||
|
|
||||||
## Widgets
|
## Widgets
|
||||||
|
|
||||||
The Kubernetes widget can show a high-level overview of the cluster,
|
The Kubernetes widget can show a high-level overview of the cluster,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||||
|
|
||||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
import { getKubeConfig } from "../../../../utils/config/kubernetes";
|
||||||
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
|
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/utils";
|
||||||
import createLogger from "../../../../utils/logger";
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("kubernetesStatsService");
|
const logger = createLogger("kubernetesStatsService");
|
||||||
@ -30,7 +30,10 @@ export default async function handler(req, res) {
|
|||||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||||
const metricsApi = new Metrics(kc);
|
const metricsApi = new Metrics(kc);
|
||||||
const podsResponse = await coreApi
|
const podsResponse = await coreApi
|
||||||
.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
.listNamespacedPod({
|
||||||
|
namespace,
|
||||||
|
labelSelector,
|
||||||
|
})
|
||||||
.then((response) => response.body)
|
.then((response) => response.body)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
import { CoreV1Api } from "@kubernetes/client-node";
|
||||||
|
|
||||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
import { getKubeConfig } from "../../../../utils/config/kubernetes";
|
||||||
import createLogger from "../../../../utils/logger";
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("kubernetesStatusService");
|
const logger = createLogger("kubernetesStatusService");
|
||||||
@ -27,8 +27,11 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||||
const podsResponse = await coreApi
|
const podsResponse = await coreApi
|
||||||
.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
.listNamespacedPod({
|
||||||
.then((response) => response.body)
|
namespace,
|
||||||
|
labelSelector,
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||||
|
|
||||||
import getKubeConfig from "../../../utils/config/kubernetes";
|
import { getKubeConfig } from "../../../utils/config/kubernetes";
|
||||||
import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils";
|
import { parseCpu, parseMemory } from "../../../utils/kubernetes/utils";
|
||||||
import createLogger from "../../../utils/logger";
|
import createLogger from "../../../utils/logger";
|
||||||
|
|
||||||
const logger = createLogger("kubernetes-widget");
|
const logger = createLogger("widget");
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
try {
|
try {
|
||||||
|
@ -2,18 +2,21 @@ import path from "path";
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig, ApiextensionsV1Api } from "@kubernetes/client-node";
|
||||||
|
|
||||||
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
|
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
|
||||||
|
|
||||||
export default function getKubeConfig() {
|
export function getKubernetes() {
|
||||||
checkAndCopyConfig("kubernetes.yaml");
|
checkAndCopyConfig("kubernetes.yaml");
|
||||||
|
|
||||||
const configFile = path.join(CONF_DIR, "kubernetes.yaml");
|
const configFile = path.join(CONF_DIR, "kubernetes.yaml");
|
||||||
const rawConfigData = readFileSync(configFile, "utf8");
|
const rawConfigData = readFileSync(configFile, "utf8");
|
||||||
const configData = substituteEnvironmentVars(rawConfigData);
|
const configData = substituteEnvironmentVars(rawConfigData);
|
||||||
const config = yaml.load(configData);
|
return yaml.load(configData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getKubeConfig = () => {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
const config = getKubernetes();
|
||||||
|
|
||||||
switch (config?.mode) {
|
switch (config?.mode) {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
@ -28,4 +31,31 @@ export default function getKubeConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return kc;
|
return kc;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkCRD(name, kc, logger) {
|
||||||
|
const apiExtensions = kc.makeApiClient(ApiextensionsV1Api);
|
||||||
|
const exist = await apiExtensions
|
||||||
|
.readCustomResourceDefinitionStatus({
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch(async (error) => {
|
||||||
|
if (error.statusCode === 403) {
|
||||||
|
logger.error(
|
||||||
|
"Error checking if CRD %s exists. Make sure to add the following permission to your RBAC: %d %s %s",
|
||||||
|
name,
|
||||||
|
error.statusCode,
|
||||||
|
error.body.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return exist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ANNOTATION_BASE = "gethomepage.dev";
|
||||||
|
export const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
|
||||||
|
export const HTTPROUTE_API_GROUP = "gateway.networking.k8s.io";
|
||||||
|
export const HTTPROUTE_API_VERSION = "v1";
|
||||||
|
@ -3,12 +3,12 @@ import path from "path";
|
|||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
import { CustomObjectsApi, NetworkingV1Api, ApiextensionsV1Api } from "@kubernetes/client-node";
|
|
||||||
|
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import checkAndCopyConfig, { CONF_DIR, getSettings, substituteEnvironmentVars } from "utils/config/config";
|
import checkAndCopyConfig, { CONF_DIR, getSettings, substituteEnvironmentVars } from "utils/config/config";
|
||||||
import getDockerArguments from "utils/config/docker";
|
import getDockerArguments from "utils/config/docker";
|
||||||
import getKubeConfig from "utils/config/kubernetes";
|
import kubernetes from "utils/kubernetes/export";
|
||||||
|
import { getKubeConfig } from "utils/config/kubernetes";
|
||||||
import * as shvl from "utils/config/shvl";
|
import * as shvl from "utils/config/shvl";
|
||||||
|
|
||||||
const logger = createLogger("service-helpers");
|
const logger = createLogger("service-helpers");
|
||||||
@ -167,36 +167,7 @@ export async function servicesFromDocker() {
|
|||||||
return mappedServiceGroups;
|
return mappedServiceGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrlFromIngress(ingress) {
|
|
||||||
const urlHost = ingress.spec.rules[0].host;
|
|
||||||
const urlPath = ingress.spec.rules[0].http.paths[0].path;
|
|
||||||
const urlSchema = ingress.spec.tls ? "https" : "http";
|
|
||||||
return `${urlSchema}://${urlHost}${urlPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkCRD(kc, name) {
|
|
||||||
const apiExtensions = kc.makeApiClient(ApiextensionsV1Api);
|
|
||||||
const exist = await apiExtensions
|
|
||||||
.readCustomResourceDefinitionStatus(name)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(async (error) => {
|
|
||||||
if (error.statusCode === 403) {
|
|
||||||
logger.error(
|
|
||||||
"Error checking if CRD %s exists. Make sure to add the following permission to your RBAC: %d %s %s",
|
|
||||||
name,
|
|
||||||
error.statusCode,
|
|
||||||
error.body.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return exist;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function servicesFromKubernetes() {
|
export async function servicesFromKubernetes() {
|
||||||
const ANNOTATION_BASE = "gethomepage.dev";
|
|
||||||
const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
|
|
||||||
const { instanceName } = getSettings();
|
const { instanceName } = getSettings();
|
||||||
|
|
||||||
checkAndCopyConfig("kubernetes.yaml");
|
checkAndCopyConfig("kubernetes.yaml");
|
||||||
@ -206,146 +177,47 @@ export async function servicesFromKubernetes() {
|
|||||||
if (!kc) {
|
if (!kc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const networking = kc.makeApiClient(NetworkingV1Api);
|
|
||||||
const crd = kc.makeApiClient(CustomObjectsApi);
|
|
||||||
|
|
||||||
const ingressList = await networking
|
// resource lists
|
||||||
.listIngressForAllNamespaces(null, null, null, null)
|
const [ingressList, traefikIngressList, httpRouteList] = await Promise.all([
|
||||||
.then((response) => response.body)
|
kubernetes.listIngress(),
|
||||||
.catch((error) => {
|
kubernetes.listTraefikIngress(),
|
||||||
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
|
kubernetes.listHttpRoute(),
|
||||||
if (error) logger.debug(error);
|
]);
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const traefikContainoExists = await checkCRD(kc, "ingressroutes.traefik.containo.us");
|
const resources = [...ingressList, ...traefikIngressList, ...httpRouteList];
|
||||||
const traefikExists = await checkCRD(kc, "ingressroutes.traefik.io");
|
|
||||||
|
|
||||||
const traefikIngressListContaino = await crd
|
if (!resources) {
|
||||||
.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
|
|
||||||
.then((response) => response.body)
|
|
||||||
.catch(async (error) => {
|
|
||||||
if (traefikContainoExists) {
|
|
||||||
logger.error(
|
|
||||||
"Error getting traefik ingresses from traefik.containo.us: %d %s %s",
|
|
||||||
error.statusCode,
|
|
||||||
error.body,
|
|
||||||
error.response,
|
|
||||||
);
|
|
||||||
if (error) logger.debug(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const traefikIngressListIo = await crd
|
|
||||||
.listClusterCustomObject("traefik.io", "v1alpha1", "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,
|
|
||||||
);
|
|
||||||
if (error) 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`],
|
|
||||||
);
|
|
||||||
ingressList.items.push(...traefikServices);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ingressList) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const services = ingressList.items
|
const services = await Promise.all(
|
||||||
.filter(
|
resources
|
||||||
(ingress) =>
|
.filter((resource) => kubernetes.isDiscoverable(resource, instanceName))
|
||||||
ingress.metadata.annotations &&
|
.map(async (resource) => kubernetes.constructedServiceFromResource(resource)),
|
||||||
ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
|
|
||||||
(!ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] ||
|
|
||||||
ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName ||
|
|
||||||
`${ANNOTATION_BASE}/instance.${instanceName}` in ingress.metadata.annotations),
|
|
||||||
)
|
|
||||||
.map((ingress) => {
|
|
||||||
let constructedService = {
|
|
||||||
app: ingress.metadata.annotations[`${ANNOTATION_BASE}/app`] || ingress.metadata.name,
|
|
||||||
namespace: ingress.metadata.namespace,
|
|
||||||
href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress),
|
|
||||||
name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name,
|
|
||||||
group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
|
|
||||||
weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
|
|
||||||
icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
|
|
||||||
description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
|
|
||||||
external: false,
|
|
||||||
type: "service",
|
|
||||||
};
|
|
||||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
|
|
||||||
constructedService.external =
|
|
||||||
String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
|
|
||||||
}
|
|
||||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`] !== undefined) {
|
|
||||||
constructedService.podSelector = ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
|
|
||||||
}
|
|
||||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
|
|
||||||
constructedService.ping = ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`];
|
|
||||||
}
|
|
||||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`]) {
|
|
||||||
constructedService.siteMonitor = ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`];
|
|
||||||
}
|
|
||||||
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
|
|
||||||
constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
|
|
||||||
}
|
|
||||||
Object.keys(ingress.metadata.annotations).forEach((annotation) => {
|
|
||||||
if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
|
|
||||||
shvl.set(
|
|
||||||
constructedService,
|
|
||||||
annotation.replace(`${ANNOTATION_BASE}/`, ""),
|
|
||||||
ingress.metadata.annotations[annotation],
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
// map service groups
|
||||||
constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
|
const mappedServiceGroups = services.reduce((groups, serverService) => {
|
||||||
} catch (e) {
|
let serverGroup = groups.find((group) => group.name === serverService.group);
|
||||||
logger.error("Error attempting k8s environment variable substitution.");
|
|
||||||
if (e) logger.debug(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return constructedService;
|
|
||||||
});
|
|
||||||
|
|
||||||
const mappedServiceGroups = [];
|
|
||||||
|
|
||||||
services.forEach((serverService) => {
|
|
||||||
let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group);
|
|
||||||
if (!serverGroup) {
|
if (!serverGroup) {
|
||||||
mappedServiceGroups.push({
|
serverGroup = {
|
||||||
name: serverService.group,
|
name: serverService.group,
|
||||||
services: [],
|
services: [],
|
||||||
});
|
};
|
||||||
serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
|
groups.push(serverGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
|
const { name: serviceName, group: _, ...pushedService } = serverService;
|
||||||
const result = {
|
|
||||||
|
serverGroup.services.push({
|
||||||
name: serviceName,
|
name: serviceName,
|
||||||
...pushedService,
|
...pushedService,
|
||||||
};
|
|
||||||
|
|
||||||
serverGroup.services.push(result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return mappedServiceGroups;
|
return mappedServiceGroups;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e) logger.error(e);
|
if (e) logger.error(e);
|
||||||
|
14
src/utils/kubernetes/export.js
Normal file
14
src/utils/kubernetes/export.js
Normal 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;
|
56
src/utils/kubernetes/httproute-list.js
Normal file
56
src/utils/kubernetes/httproute-list.js
Normal 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;
|
||||||
|
}
|
26
src/utils/kubernetes/ingress-list.js
Normal file
26
src/utils/kubernetes/ingress-list.js
Normal 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;
|
||||||
|
}
|
130
src/utils/kubernetes/resource-helpers.js
Normal file
130
src/utils/kubernetes/resource-helpers.js
Normal 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;
|
||||||
|
}
|
70
src/utils/kubernetes/traefik-list.js
Normal file
70
src/utils/kubernetes/traefik-list.js
Normal 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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user