Sub columns for services

This commit is contained in:
lukylix 2023-07-05 21:52:59 +02:00
parent d1f83c0359
commit 02f24d3abd
2 changed files with 127 additions and 68 deletions

View File

@ -14,17 +14,29 @@ const columnMap = [
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8", "grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
]; ];
export default function List({ group, services, layout }) { export default function List({ group, services, layout, isGroup = false }) {
//console.log({ group, services, layout });
console.log({ services });
return ( return (
<ul <ul
className={classNames( className={classNames(
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col", layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
"mt-3" isGroup ? undefined : "mt-3"
)} )}
> >
{services.map((service) => ( {services.map((service, i) => {
return service.type == "grouped-service" ? (
<List
key={service.name}
group={service.name}
services={service.services}
layout={{ columns: parseInt(service.name) || service.services.length, style: "row" }}
isGroup={true}
/>
) : (
<Item key={service.container ?? service.app ?? service.name} service={service} group={group} /> <Item key={service.container ?? service.app ?? service.name} service={service} group={group} />
))} );
})}
</ul> </ul>
); );
} }

View File

@ -13,7 +13,6 @@ import getKubeConfig from "utils/config/kubernetes";
const logger = createLogger("service-helpers"); const logger = createLogger("service-helpers");
export async function servicesFromConfig() { export async function servicesFromConfig() {
checkAndCopyConfig("services.yaml"); checkAndCopyConfig("services.yaml");
@ -25,23 +24,43 @@ export async function servicesFromConfig() {
if (!services) { if (!services) {
return []; return [];
} }
// map easy to write YAML objects into easy to consume JS arrays // map easy to write YAML objects into easy to consume JS arrays
const servicesArray = services.map((servicesGroup) => ({ const servicesArray = services.map((servicesGroup) => ({
name: Object.keys(servicesGroup)[0], name: Object.keys(servicesGroup)[0],
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => ({ services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => {
if (Array.isArray(entries[Object.keys(entries)[0]]))
return {
name: Object.keys(entries)[0],
services: entries[Object.keys(entries)[0]].map((entry) => ({
name: Object.keys(entry)[0],
...entry[Object.keys(entry)[0]],
type: "service",
})),
type: "grouped-service",
};
return {
name: Object.keys(entries)[0], name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]], ...entries[Object.keys(entries)[0]],
type: 'service' type: "service",
})), };
}),
})); }));
// add default weight to services based on their position in the configuration // add default weight to services based on their position in the configuration
servicesArray.forEach((group, groupIndex) => { servicesArray.forEach((group, groupIndex) => {
group.services.forEach((service, serviceIndex) => { group.services.forEach((service, serviceIndex) => {
if(!service.weight) { if (!service.weight && service.type !== "grouped-service") {
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100; servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
} }
if (service.type === "grouped-service") {
service.weight = (serviceIndex + 1) * 100;
service.services.forEach((groupedService, groupedServiceIndex) => {
if (!groupedService.weight) {
servicesArray[groupIndex].services[serviceIndex].services[groupedServiceIndex].weight =
(groupedServiceIndex + 1) * 100;
}
});
}
}); });
}); });
@ -66,7 +85,9 @@ export async function servicesFromDocker() {
const isSwarm = !!servers[serverName].swarm; const isSwarm = !!servers[serverName].swarm;
const docker = new Docker(getDockerArguments(serverName).conn); const docker = new Docker(getDockerArguments(serverName).conn);
const listProperties = { all: true }; const listProperties = { all: true };
const containers = await ((isSwarm) ? docker.listServices(listProperties) : docker.listContainers(listProperties)); const containers = await (isSwarm
? docker.listServices(listProperties)
: docker.listContainers(listProperties));
// bad docker connections can result in a <Buffer ...> object? // bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array // in any case, this ensures the result is the expected array
@ -76,8 +97,8 @@ export async function servicesFromDocker() {
const discovered = containers.map((container) => { const discovered = containers.map((container) => {
let constructedService = null; let constructedService = null;
const containerLabels = isSwarm ? shvl.get(container, 'Spec.Labels') : container.Labels; const containerLabels = isSwarm ? shvl.get(container, "Spec.Labels") : container.Labels;
const containerName = isSwarm ? shvl.get(container, 'Spec.Name') : container.Names[0]; const containerName = isSwarm ? shvl.get(container, "Spec.Name") : container.Names[0];
Object.keys(containerLabels).forEach((label) => { Object.keys(containerLabels).forEach((label) => {
if (label.startsWith("homepage.")) { if (label.startsWith("homepage.")) {
@ -85,10 +106,14 @@ export async function servicesFromDocker() {
constructedService = { constructedService = {
container: containerName.replace(/^\//, ""), container: containerName.replace(/^\//, ""),
server: serverName, server: serverName,
type: 'service' type: "service",
}; };
} }
shvl.set(constructedService, label.replace("homepage.", ""), substituteEnvironmentVars(containerLabels[label])); shvl.set(
constructedService,
label.replace("homepage.", ""),
substituteEnvironmentVars(containerLabels[label])
);
} }
}); });
@ -132,12 +157,12 @@ export async function servicesFromDocker() {
function getUrlFromIngress(ingress) { function getUrlFromIngress(ingress) {
const urlHost = ingress.spec.rules[0].host; const urlHost = ingress.spec.rules[0].host;
const urlPath = ingress.spec.rules[0].http.paths[0].path; const urlPath = ingress.spec.rules[0].http.paths[0].path;
const urlSchema = ingress.spec.tls ? 'https' : 'http'; const urlSchema = ingress.spec.tls ? "https" : "http";
return `${urlSchema}://${urlHost}${urlPath}`; return `${urlSchema}://${urlHost}${urlPath}`;
} }
export async function servicesFromKubernetes() { export async function servicesFromKubernetes() {
const ANNOTATION_BASE = 'gethomepage.dev'; const ANNOTATION_BASE = "gethomepage.dev";
const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`; const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
const ANNOTATION_POD_SELECTOR = `${ANNOTATION_BASE}/pod-selector`; const ANNOTATION_POD_SELECTOR = `${ANNOTATION_BASE}/pod-selector`;
@ -151,23 +176,36 @@ export async function servicesFromKubernetes() {
const networking = kc.makeApiClient(NetworkingV1Api); const networking = kc.makeApiClient(NetworkingV1Api);
const crd = kc.makeApiClient(CustomObjectsApi); const crd = kc.makeApiClient(CustomObjectsApi);
const ingressList = await networking.listIngressForAllNamespaces(null, null, null, null) const ingressList = await networking
.listIngressForAllNamespaces(null, null, null, null)
.then((response) => response.body) .then((response) => response.body)
.catch((error) => { .catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response); logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
return null; return null;
}); });
const traefikIngressList = await crd.listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes") const traefikIngressList = await crd
.listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes")
.then((response) => response.body) .then((response) => response.body)
.catch(async (error) => { .catch(async (error) => {
logger.error("Error getting traefik ingresses from traefik.io: %d %s %s", error.statusCode, error.body, error.response); logger.error(
"Error getting traefik ingresses from traefik.io: %d %s %s",
error.statusCode,
error.body,
error.response
);
// Fallback to the old traefik CRD group // Fallback to the old traefik CRD group
const fallbackIngressList = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes") const fallbackIngressList = await crd
.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
.then((response) => response.body) .then((response) => response.body)
.catch((fallbackError) => { .catch((fallbackError) => {
logger.error("Error getting traefik ingresses from traefik.containo.us: %d %s %s", fallbackError.statusCode, fallbackError.body, fallbackError.response); logger.error(
"Error getting traefik ingresses from traefik.containo.us: %d %s %s",
fallbackError.statusCode,
fallbackError.body,
fallbackError.response
);
return null; return null;
}); });
@ -175,8 +213,9 @@ export async function servicesFromKubernetes() {
}); });
if (traefikIngressList && traefikIngressList.items.length > 0) { if (traefikIngressList && traefikIngressList.items.length > 0) {
const traefikServices = traefikIngressList.items const traefikServices = traefikIngressList.items.filter(
.filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]) (ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]
);
ingressList.items.push(...traefikServices); ingressList.items.push(...traefikServices);
} }
@ -184,7 +223,10 @@ export async function servicesFromKubernetes() {
return []; return [];
} }
const services = ingressList.items const services = ingressList.items
.filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === 'true') .filter(
(ingress) =>
ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true"
)
.map((ingress) => { .map((ingress) => {
let constructedService = { let constructedService = {
app: ingress.metadata.name, app: ingress.metadata.name,
@ -192,14 +234,15 @@ export async function servicesFromKubernetes() {
href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress), href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress),
name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name, name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name,
group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes", group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || '0', weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || '', icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || '', description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
external: false, external: false,
type: 'service' type: "service",
}; };
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) { if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
constructedService.external = String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true" constructedService.external =
String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
} }
if (ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]) { if (ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]) {
constructedService.podSelector = ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]; constructedService.podSelector = ingress.metadata.annotations[ANNOTATION_POD_SELECTOR];
@ -209,7 +252,11 @@ export async function servicesFromKubernetes() {
} }
Object.keys(ingress.metadata.annotations).forEach((annotation) => { Object.keys(ingress.metadata.annotations).forEach((annotation) => {
if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) { if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
shvl.set(constructedService, annotation.replace(`${ANNOTATION_BASE}/`, ""), ingress.metadata.annotations[annotation]); shvl.set(
constructedService,
annotation.replace(`${ANNOTATION_BASE}/`, ""),
ingress.metadata.annotations[annotation]
);
} }
}); });
@ -244,7 +291,6 @@ export async function servicesFromKubernetes() {
}); });
return mappedServiceGroups; return mappedServiceGroups;
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
throw e; throw e;
@ -257,7 +303,7 @@ export function cleanServiceGroups(groups) {
services: serviceGroup.services.map((service) => { services: serviceGroup.services.map((service) => {
const cleanedService = { ...service }; const cleanedService = { ...service };
if (cleanedService.showStats !== undefined) cleanedService.showStats = JSON.parse(cleanedService.showStats); if (cleanedService.showStats !== undefined) cleanedService.showStats = JSON.parse(cleanedService.showStats);
if (typeof service.weight === 'string') { if (typeof service.weight === "string") {
const weight = parseInt(service.weight, 10); const weight = parseInt(service.weight, 10);
if (Number.isNaN(weight)) { if (Number.isNaN(weight)) {
cleanedService.weight = 0; cleanedService.weight = 0;
@ -292,9 +338,10 @@ export function cleanServiceGroups(groups) {
} = cleanedService.widget; } = cleanedService.widget;
let fieldsList = fields; let fieldsList = fields;
if (typeof fields === 'string') { if (typeof fields === "string") {
try { JSON.parse(fields) } try {
catch (e) { JSON.parse(fields);
} catch (e) {
logger.error("Invalid fields list detected in config for service '%s'", service.name); logger.error("Invalid fields list detected in config for service '%s'", service.name);
fieldsList = null; fieldsList = null;
} }