diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 97f61ea0..c1f2d5ce 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -1,10 +1,9 @@ /* eslint-disable no-console */ -import { promises as fs } from "fs"; -import path from "path"; - -import yaml from "js-yaml"; - -import checkAndCopyConfig, { getSettings, substituteEnvironmentVars, CONF_DIR } from "utils/config/config"; +import {getSettings} from "utils/config/config"; +import { + bookmarksFromConfig, + bookmarksFromDocker +} from "utils/config/bookmark-helpers"; import { servicesFromConfig, servicesFromDocker, @@ -25,16 +24,29 @@ function compareServices(service1, service2) { } export async function bookmarksResponse() { - 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 []; - + let discoveredDockerBookmarks; + let configuredBookmarks; 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 { initialSettings = await getSettings(); @@ -44,26 +56,39 @@ export async function bookmarksResponse() { 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 unsortedGroups = []; const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null; - bookmarksArray.forEach((group) => { - if (definedLayouts) { - const layoutIndex = definedLayouts.findIndex((layout) => layout === group.name); - if (layoutIndex > -1) sortedGroups[layoutIndex] = group; - else unsortedGroups.push(group); + const mergedGroupsNames = [ + ...new Set( + [ + discoveredDockerBookmarks.map((group) => group.name), + 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 { - unsortedGroups.push(group); + unsortedGroups.push(mergedGroup); } }); diff --git a/src/utils/config/bookmark-helpers.js b/src/utils/config/bookmark-helpers.js new file mode 100644 index 00000000..3397d1df --- /dev/null +++ b/src/utils/config/bookmark-helpers.js @@ -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 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; +}