This commit is contained in:
shamoon 2023-12-17 23:58:43 -08:00
parent 36f4bdc26d
commit 0fe21d9bf7
5 changed files with 206 additions and 294 deletions

View File

@ -5,6 +5,7 @@ import { SettingsContext } from "utils/contexts/settings";
import ResolvedIcon from "components/resolvedicon"; import ResolvedIcon from "components/resolvedicon";
export default function Item({ bookmark }) { export default function Item({ bookmark }) {
console.log(bookmark);
const description = bookmark.description ?? new URL(bookmark.href).hostname; const description = bookmark.description ?? new URL(bookmark.href).hostname;
const { settings } = useContext(SettingsContext); const { settings } = useContext(SettingsContext);

View File

@ -1,9 +1,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import {getSettings} from "utils/config/config"; import { getSettings } from "utils/config/config";
import { import { bookmarksFromConfig, bookmarksFromDocker } from "utils/config/bookmark-helpers";
bookmarksFromConfig,
bookmarksFromDocker
} from "utils/config/bookmark-helpers";
import { import {
servicesFromConfig, servicesFromConfig,
servicesFromDocker, servicesFromDocker,
@ -27,15 +24,13 @@ export async function bookmarksResponse() {
let discoveredDockerBookmarks; let discoveredDockerBookmarks;
let configuredBookmarks; let configuredBookmarks;
let initialSettings; let initialSettings;
console.debug("Bookmark response called");
try { try {
discoveredDockerBookmarks = await bookmarksFromDocker(); discoveredDockerBookmarks = await bookmarksFromDocker();
if (discoveredDockerBookmarks?.length === 0) { if (discoveredDockerBookmarks?.length === 0) {
console.debug("No containers were found with bookmark labels"); console.debug("No containers were found with homepage bookmark labels");
} }
} catch (e) { } catch (e) {
console.error("Failed to discover bookmarks, please check docker.yaml for errors or remove example entries.") console.error("Failed to discover bookmarks, please check docker.yaml for errors or remove example entries.");
if (e) console.error(e.toString()); if (e) console.error(e.toString());
discoveredDockerBookmarks = []; discoveredDockerBookmarks = [];
} }
@ -62,26 +57,20 @@ export async function bookmarksResponse() {
const mergedGroupsNames = [ const mergedGroupsNames = [
...new Set( ...new Set(
[ [discoveredDockerBookmarks.map((group) => group.name), configuredBookmarks.map((group) => group.name)].flat(),
discoveredDockerBookmarks.map((group) => group.name), ),
configuredBookmarks.map((group) => group.name), ];
].flat(),
)
]
mergedGroupsNames.forEach((groupName) => { mergedGroupsNames.forEach((groupName) => {
const discoveredDockerGroup = discoveredDockerBookmarks.find((group) => group.name === groupName) || {
const discoveredDockerGroup = discoveredDockerBookmarks.find((group) => group.name === groupName) || {
bookmarks: [], bookmarks: [],
}; };
const configuredGroup = configuredBookmarks.find((group) => group.name === groupName) || { bookmarks: [] }; const configuredGroup = configuredBookmarks.find((group) => group.name === groupName) || { bookmarks: [] };
const mergedGroup = { const mergedGroup = {
name: groupName, name: groupName,
bookmarks: [...discoveredDockerGroup.bookmarks, ...configuredGroup.bookmarks] bookmarks: [...discoveredDockerGroup.bookmarks, ...configuredGroup.bookmarks].filter((bookmark) => bookmark),
.filter((bookmark) => bookmark)
// .sort(compareBookmarks), // TODO is a sort needed? // .sort(compareBookmarks), // TODO is a sort needed?
} };
if (definedLayouts) { if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name); const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);

View File

@ -2,17 +2,24 @@ import { promises as fs } from "fs";
import path from "path"; import path from "path";
import yaml from "js-yaml"; import yaml from "js-yaml";
import Docker from "dockerode";
import { CustomObjectsApi, NetworkingV1Api } from "@kubernetes/client-node";
import {
getDockerServers,
listDockerContainers,
getUrlFromIngress,
mapObjectsToGroup,
getIngressList,
ANNOTATION_BASE,
ANNOTATION_WIDGET_BASE,
} from "./integration-helpers";
import createLogger from "utils/logger";
import checkAndCopyConfig, { getSettings, CONF_DIR, substituteEnvironmentVars } from "utils/config/config"; import checkAndCopyConfig, { getSettings, CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
import getDockerArguments from "utils/config/docker";
import getKubeConfig from "utils/config/kubernetes";
import { checkCRD, getUrlFromIngress } from "./service-helpers";
import * as shvl from "utils/config/shvl"; import * as shvl from "utils/config/shvl";
export async function bookmarksFromConfig() { const logger = createLogger("bookmark-helpers");
export async function bookmarksFromConfig() {
checkAndCopyConfig("bookmarks.yaml"); checkAndCopyConfig("bookmarks.yaml");
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml"); const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
@ -32,62 +39,9 @@ export async function bookmarksFromConfig() {
})); }));
return bookmarksArray; return bookmarksArray;
}
const getDockerServers = async () => {
checkAndCopyConfig("docker.yaml");
const dockerYaml = path.join(CONF_DIR, "docker.yaml");
const rawDockerFileContents = await fs.readFile(dockerYaml, "utf8");
const dockerFileContents = substituteEnvironmentVars(rawDockerFileContents);
return yaml.load(dockerFileContents);
}
const listDockerContainers = async (servers, serverName) => {
const isSwarm = !!servers[serverName].swarm;
const docker = new Docker(getDockerArguments(serverName).conn);
const listProperties = { all: true };
const containers = await (isSwarm
? docker.listbookmarks(listProperties)
: docker.listContainers(listProperties));
// bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array
if (!Array.isArray(containers)) {
return [];
}
return containers;
}
const mapObjectsToGroup = (servers, objectName) => {
const mappedObjectGroups = [];
servers.forEach((server) => {
server[objectName].forEach((serverObject) => {
let serverGroup = mappedObjectGroups.find((searchedGroup) => searchedGroup.name === serverObject.group);
if (!serverGroup) {
const gObject = {name: serverObject.group}
gObject[objectName] = []
mappedObjectGroups.push(gObject);
serverGroup = mappedObjectGroups[mappedObjectGroups.length - 1];
}
const { name: serverObjectName, group: serverObjectGroup, ...pushedObject } = serverObject;
const result = {
name: serverObjectName,
...pushedObject,
};
serverGroup[objectName].push(result);
});
});
return mappedObjectGroups;
} }
export async function bookmarksFromDocker() { export async function bookmarksFromDocker() {
const servers = await getDockerServers(); const servers = await getDockerServers();
if (!servers) { if (!servers) {
@ -109,9 +63,8 @@ export async function bookmarksFromDocker() {
Object.keys(containerLabels).forEach((label) => { Object.keys(containerLabels).forEach((label) => {
if (label.startsWith("homepage.bookmarks.")) { if (label.startsWith("homepage.bookmarks.")) {
const containerNameNoSlash = containerName.replace(/^\//, ""); const containerNameNoSlash = containerName.replace(/^\//, "");
const cleanLabel = label.replace(`homepage.bookmarks.${containerNameNoSlash}.`, "");
let value = cleanLabel; let value = label.replace(`homepage.bookmarks.${containerNameNoSlash}.`, "");
if (instanceName && value.startsWith(`instance.${instanceName}.`)) { if (instanceName && value.startsWith(`instance.${instanceName}.`)) {
value = value.replace(`instance.${instanceName}.`, ""); value = value.replace(`instance.${instanceName}.`, "");
} else if (value.startsWith("instance.")) { } else if (value.startsWith("instance.")) {
@ -140,76 +93,12 @@ export async function bookmarksFromDocker() {
}), }),
); );
const mappedBookmarkGroups = mapObjectsToGroup(bookmarkServers, 'bookmarks'); return mapObjectsToGroup(bookmarkServers, "bookmarks");
return mappedBookmarkGroups;
} }
export async function bookmarksFromKubernetes() { export async function bookmarksFromKubernetes() {
const ANNOTATION_BASE = "gethomepage.dev";
const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
checkAndCopyConfig("kubernetes.yaml");
try { try {
const kc = getKubeConfig(); const ingressList = await getIngressList();
if (!kc) {
return [];
}
const networking = kc.makeApiClient(NetworkingV1Api);
const crd = kc.makeApiClient(CustomObjectsApi);
const ingressList = await networking
.listIngressForAllNamespaces(null, null, null, null)
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
return null;
});
const traefikContainoExists = await checkCRD(kc, "ingressroutes.traefik.containo.us");
const traefikExists = await checkCRD(kc, "ingressroutes.traefik.io");
const traefikIngressListContaino = await crd
.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,
);
}
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,
);
}
return [];
});
const traefikIngressList = [...(traefikIngressListContaino?.items ?? []), ...(traefikIngressListIo?.items ?? [])];
if (traefikIngressList.length > 0) {
const traefikbookmarks = traefikIngressList.filter(
(ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`],
);
ingressList.items.push(...traefikbookmarks);
}
if (!ingressList) { if (!ingressList) {
return []; return [];
@ -230,9 +119,6 @@ export async function bookmarksFromKubernetes() {
description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "", description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
type: "bookmark", type: "bookmark",
}; };
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`]) {
constructedBookmark.podSelector = ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
}
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( shvl.set(
@ -279,4 +165,3 @@ export async function bookmarksFromKubernetes() {
throw e; throw e;
} }
} }

View File

@ -0,0 +1,161 @@
import { promises as fs } from "fs";
import path from "path";
import Docker from "dockerode";
import yaml from "js-yaml";
import { CustomObjectsApi, NetworkingV1Api, ApiextensionsV1Api } from "@kubernetes/client-node";
import getKubeConfig from "utils/config/kubernetes";
import createLogger from "utils/logger";
import getDockerArguments from "utils/config/docker";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
const logger = createLogger("integration-helpers");
export async function getDockerServers() {
checkAndCopyConfig("docker.yaml");
const dockerYaml = path.join(CONF_DIR, "docker.yaml");
const rawDockerFileContents = await fs.readFile(dockerYaml, "utf8");
const dockerFileContents = substituteEnvironmentVars(rawDockerFileContents);
return yaml.load(dockerFileContents);
}
export async function listDockerContainers(servers, serverName) {
const isSwarm = !!servers[serverName].swarm;
const docker = new Docker(getDockerArguments(serverName).conn);
const listProperties = { all: true };
const containers = await (isSwarm ? docker.listbookmarks(listProperties) : docker.listContainers(listProperties));
// bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array
if (!Array.isArray(containers)) {
return [];
}
return containers;
}
export function mapObjectsToGroup(servers, objectName) {
const mappedObjectGroups = [];
servers.forEach((server) => {
server[objectName].forEach((serverObject) => {
let serverGroup = mappedObjectGroups.find((searchedGroup) => searchedGroup.name === serverObject.group);
if (!serverGroup) {
const gObject = { name: serverObject.group };
gObject[objectName] = [];
mappedObjectGroups.push(gObject);
serverGroup = mappedObjectGroups[mappedObjectGroups.length - 1];
}
const { name: serverObjectName, group: serverObjectGroup, ...pushedObject } = serverObject;
const result = {
name: serverObjectName,
...pushedObject,
};
serverGroup[objectName].push(result);
});
});
return mappedObjectGroups;
}
export 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 const ANNOTATION_BASE = "gethomepage.dev";
export const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
export async function getIngressList() {
checkAndCopyConfig("kubernetes.yaml");
try {
const kc = getKubeConfig();
if (!kc) {
return [];
}
const networking = kc.makeApiClient(NetworkingV1Api);
const crd = kc.makeApiClient(CustomObjectsApi);
const ingressList = await networking
.listIngressForAllNamespaces(null, null, null, null)
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
return null;
});
const traefikContainoExists = await checkCRD(kc, "ingressroutes.traefik.containo.us");
const traefikExists = await checkCRD(kc, "ingressroutes.traefik.io");
const traefikIngressListContaino = await crd
.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,
);
}
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,
);
}
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);
}
return ingressList;
} catch (e) {
logger.error(e);
throw e;
}
}

View File

@ -2,13 +2,19 @@ import { promises as fs } from "fs";
import path from "path"; import path from "path";
import yaml from "js-yaml"; import yaml from "js-yaml";
import Docker from "dockerode";
import { CustomObjectsApi, NetworkingV1Api, ApiextensionsV1Api } from "@kubernetes/client-node"; import {
getDockerServers,
listDockerContainers,
getUrlFromIngress,
mapObjectsToGroup,
getIngressList,
ANNOTATION_BASE,
ANNOTATION_WIDGET_BASE,
} from "./integration-helpers";
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 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");
@ -48,12 +54,7 @@ export async function servicesFromConfig() {
} }
export async function servicesFromDocker() { export async function servicesFromDocker() {
checkAndCopyConfig("docker.yaml"); const servers = await getDockerServers();
const dockerYaml = path.join(CONF_DIR, "docker.yaml");
const rawDockerFileContents = await fs.readFile(dockerYaml, "utf8");
const dockerFileContents = substituteEnvironmentVars(rawDockerFileContents);
const servers = yaml.load(dockerFileContents);
if (!servers) { if (!servers) {
return []; return [];
@ -65,18 +66,7 @@ export async function servicesFromDocker() {
Object.keys(servers).map(async (serverName) => { Object.keys(servers).map(async (serverName) => {
try { try {
const isSwarm = !!servers[serverName].swarm; const isSwarm = !!servers[serverName].swarm;
const docker = new Docker(getDockerArguments(serverName).conn); const containers = await listDockerContainers(servers, serverName);
const listProperties = { all: true };
const containers = await (isSwarm
? docker.listServices(listProperties)
: docker.listContainers(listProperties));
// bad docker connections can result in a <Buffer ...> object?
// in any case, this ensures the result is the expected array
if (!Array.isArray(containers)) {
return [];
}
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;
@ -113,127 +103,13 @@ export async function servicesFromDocker() {
}), }),
); );
const mappedServiceGroups = []; return mapObjectsToGroup(serviceServers, "services");
serviceServers.forEach((server) => {
server.services.forEach((serverService) => {
let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group);
if (!serverGroup) {
mappedServiceGroups.push({
name: serverService.group,
services: [],
});
serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
}
const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
const result = {
name: serviceName,
...pushedService,
};
serverGroup.services.push(result);
});
});
return mappedServiceGroups;
}
export 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();
checkAndCopyConfig("kubernetes.yaml");
try { try {
const kc = getKubeConfig(); const ingressList = await getIngressList();
if (!kc) { if (!ingressList?.items) {
return [];
}
const networking = kc.makeApiClient(NetworkingV1Api);
const crd = kc.makeApiClient(CustomObjectsApi);
const ingressList = await networking
.listIngressForAllNamespaces(null, null, null, null)
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
return null;
});
const traefikContainoExists = await checkCRD(kc, "ingressroutes.traefik.containo.us");
const traefikExists = await checkCRD(kc, "ingressroutes.traefik.io");
const traefikIngressListContaino = await crd
.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,
);
}
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,
);
}
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 = ingressList.items