Feature: Added discover bookmarks from compose file (#1930)

This commit is contained in:
Riccardi Dalexis 2023-11-23 14:44:24 -05:00 committed by shamoon
parent b9f1ddd284
commit 2fa46066ae
2 changed files with 193 additions and 30 deletions

View File

@ -1,10 +1,9 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { promises as fs } from "fs"; import {getSettings} from "utils/config/config";
import path from "path"; import {
bookmarksFromConfig,
import yaml from "js-yaml"; bookmarksFromDocker
} from "utils/config/bookmark-helpers";
import checkAndCopyConfig, { getSettings, substituteEnvironmentVars, CONF_DIR } from "utils/config/config";
import { import {
servicesFromConfig, servicesFromConfig,
servicesFromDocker, servicesFromDocker,
@ -25,16 +24,29 @@ function compareServices(service1, service2) {
} }
export async function bookmarksResponse() { export async function bookmarksResponse() {
checkAndCopyConfig("bookmarks.yaml"); let discoveredDockerBookmarks;
let configuredBookmarks;
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
const rawFileContents = await fs.readFile(bookmarksYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const bookmarks = yaml.load(fileContents);
if (!bookmarks) return [];
let initialSettings; let initialSettings;
console.debug("Bookmark response called");
try {
discoveredDockerBookmarks = await bookmarksFromDocker();
if (discoveredDockerBookmarks?.length === 0) {
console.debug("No containers were found with bookmark labels");
}
} catch (e) {
console.error("Failed to discover bookmarks, please check docker.yaml for errors or remove example entries.")
if (e) console.error(e.toString());
discoveredDockerBookmarks = [];
}
try {
configuredBookmarks = await bookmarksFromConfig();
} catch (e) {
console.error("Failed to load bookmarks.yaml");
if (e) console.error(e.toString());
configuredBookmarks = [];
}
try { try {
initialSettings = await getSettings(); initialSettings = await getSettings();
@ -44,26 +56,39 @@ export async function bookmarksResponse() {
initialSettings = {}; initialSettings = {};
} }
// map easy to write YAML objects into easy to consume JS arrays
const bookmarksArray = bookmarks.map((group) => ({
name: Object.keys(group)[0],
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]][0],
})),
}));
const sortedGroups = []; const sortedGroups = [];
const unsortedGroups = []; const unsortedGroups = [];
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null; const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
bookmarksArray.forEach((group) => { const mergedGroupsNames = [
if (definedLayouts) { ...new Set(
const layoutIndex = definedLayouts.findIndex((layout) => layout === group.name); [
if (layoutIndex > -1) sortedGroups[layoutIndex] = group; discoveredDockerBookmarks.map((group) => group.name),
else unsortedGroups.push(group); configuredBookmarks.map((group) => group.name),
].flat(),
)
]
mergedGroupsNames.forEach((groupName) => {
const discoveredDockerGroup = discoveredDockerBookmarks.find((group) => group.name === groupName) || {
bookmarks: [],
};
const configuredGroup = configuredBookmarks.find((group) => group.name === groupName) || { bookmarks: [] };
const mergedGroup = {
name: groupName,
bookmarks: [...discoveredDockerGroup.bookmarks, ...configuredGroup.bookmarks]
.filter((bookmark) => bookmark)
// .sort(compareBookmarks), // TODO is a sort needed?
}
if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
else unsortedGroups.push(mergedGroup);
} else { } else {
unsortedGroups.push(group); unsortedGroups.push(mergedGroup);
} }
}); });

View File

@ -0,0 +1,138 @@
import { promises as fs } from "fs";
import path from "path";
import yaml from "js-yaml";
import Docker from "dockerode";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
import getDockerArguments from "utils/config/docker";
import * as shvl from "utils/config/shvl";
export async function bookmarksFromConfig() {
checkAndCopyConfig("bookmarks.yaml");
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
const rawFileContents = await fs.readFile(bookmarksYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const bookmarks = yaml.load(fileContents);
if (!bookmarks) return [];
// map easy to write YAML objects into easy to consume JS arrays
const bookmarksArray = bookmarks.map((group) => ({
name: Object.keys(group)[0],
bookmarks: group[Object.keys(group)[0]].map((entries) => ({
name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]][0],
})),
}));
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.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 [];
}
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() {
const servers = await getDockerServers();
if (!servers) {
return [];
}
const bookmarkServers = await Promise.all(
Object.keys(servers).map(async (serverName) => {
try {
const isSwarm = !!servers[serverName].swarm;
const containers = await listDockerContainers(servers, serverName);
const discovered = containers.map((container) => {
let constructedBookmark = null;
const containerLabels = isSwarm ? shvl.get(container, "Spec.Labels") : container.Labels;
Object.keys(containerLabels).forEach((label) => {
if (label.startsWith("homepage.bookmarks.")) {
const cleanLabel = label.replace("homepage.bookmarks.", "");
// homepage.bookmarks.this_is_bookmark_group.this_is_bookmark_name.abbr = "href"
// this_is_bookmark_group.this_is_bookmark_name.abbr = "href"
// TODO should I add error handling for badly formatted labels?
const [bookmarkGroup, bookmarkName, bookmarkAbbr] = cleanLabel.split('.');
const bookmarkHref= containerLabels[label];
constructedBookmark = {
group: bookmarkGroup,
name: bookmarkName,
href: bookmarkHref,
abbr: bookmarkAbbr,
type: "bookmark"
};
}
});
return constructedBookmark;
});
return { server: serverName, bookmarks: discovered.filter((filteredBookmark) => filteredBookmark) };
} catch (e) {
// a server failed, but others may succeed
return { server: serverName, bookmarks: [] };
}
}),
);
const mappedBookmarkGroups = mapObjectsToGroup(bookmarkServers, 'bookmarks');
return mappedBookmarkGroups;
}