Performance: fetch all docker container statuses in one request (#7092)

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
DaddyBoard
2026-09-05 15:17:52 +00:00
committed by GitHub
co-authored by shamoon
parent df34927198
commit 438788240b
11 changed files with 617 additions and 365 deletions
+55 -21
View File
@@ -60,6 +60,46 @@ export async function servicesFromConfig() {
return parseServicesToGroups(services);
}
function flattenServices(groups, services = []) {
groups.forEach((group) => {
(group.services ?? []).forEach((service) => services.push(service));
flattenServices(group.groups ?? [], services);
});
return services;
}
function dockerWidgets(service) {
const widgets = service.widget ? [service.widget, ...(service.widgets ?? [])] : (service.widgets ?? []);
return widgets.filter((widget) => widget?.type === "docker");
}
export async function containersFromConfig(server) {
const target = server || "";
const services = flattenServices(await servicesFromConfig());
// services and docker widgets both carry container + server
const refs = services.flatMap((service) => [service, ...dockerWidgets(service)]);
const matching = refs.filter((ref) => ref.container && (ref.server || "") === target);
return new Set(matching.map((ref) => ref.container));
}
// homepage.foo -> foo, homepage.instance.<this instance>.foo -> foo, another instance -> null
export function homepageLabelValue(label, instanceName) {
if (!label.startsWith("homepage.")) return null;
const value = label.replace("homepage.", "");
if (!value.startsWith("instance.")) return value;
if (instanceName && value.startsWith(`instance.${instanceName}.`)) {
return value.replace(`instance.${instanceName}.`, "");
}
return null;
}
export function hasHomepageLabels(labels, instanceName) {
return Object.keys(labels ?? {}).some((label) => homepageLabelValue(label, instanceName) !== null);
}
export async function servicesFromDocker() {
checkAndCopyConfig("docker.yaml");
@@ -95,29 +135,23 @@ export async function servicesFromDocker() {
const containerLabels = isSwarm ? shvl.get(container, "Spec.Labels") : container.Labels;
const containerName = isSwarm ? shvl.get(container, "Spec.Name") : container.Names[0];
Object.keys(containerLabels).forEach((label) => {
if (label.startsWith("homepage.")) {
let value = label.replace("homepage.", "");
if (instanceName && value.startsWith(`instance.${instanceName}.`)) {
value = value.replace(`instance.${instanceName}.`, "");
} else if (value.startsWith("instance.")) {
return;
}
Object.keys(containerLabels ?? {}).forEach((label) => {
const value = homepageLabelValue(label, instanceName);
if (value === null) return;
if (!constructedService) {
constructedService = {
container: containerName.replace(/^\//, ""),
server: serverName,
weight: 0,
type: "service",
};
}
let substitutedVal = substituteEnvironmentVars(containerLabels[label]);
if (value === "widget.version" || /^widgets\[\d+\]\.version$/.test(value)) {
substitutedVal = parseVersionForUrl(substitutedVal);
}
shvl.set(constructedService, value, substitutedVal);
if (!constructedService) {
constructedService = {
container: containerName.replace(/^\//, ""),
server: serverName,
weight: 0,
type: "service",
};
}
let substitutedVal = substituteEnvironmentVars(containerLabels[label]);
if (value === "widget.version" || /^widgets\[\d+\]\.version$/.test(value)) {
substitutedVal = parseVersionForUrl(substitutedVal);
}
shvl.set(constructedService, value, substitutedVal);
});
if (constructedService && (!constructedService.name || !constructedService.group)) {
+113
View File
@@ -103,6 +103,119 @@ describe("utils/config/service-helpers", () => {
expect(await mod.servicesFromConfig()).toEqual([]);
});
it("containersFromConfig returns an empty set when services.yaml is empty", async () => {
state.servicesYaml = null;
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("local")).toEqual(new Set());
});
it("containersFromConfig only returns containers for the requested server", async () => {
state.servicesYaml = [
{ Group: [{ App: { container: "app", server: "local" } }, { Other: { container: "other", server: "remote" } }] },
];
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("local")).toEqual(new Set(["app"]));
expect(await mod.containersFromConfig("remote")).toEqual(new Set(["other"]));
});
it("containersFromConfig maps a service without a server to the default connection", async () => {
state.servicesYaml = [{ Group: [{ App: { container: "app" } }] }];
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("")).toEqual(new Set(["app"]));
expect(await mod.containersFromConfig(undefined)).toEqual(new Set(["app"]));
expect(await mod.containersFromConfig("local")).toEqual(new Set());
});
it("containersFromConfig recurses into nested groups", async () => {
state.servicesYaml = [
{
Main: [
{ Child: [{ Deep: { container: "deep", server: "local" } }] },
{ Root: { container: "top", server: "local" } },
],
},
];
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("local")).toEqual(new Set(["top", "deep"]));
});
it("containersFromConfig includes docker widget containers in both widget and widgets forms", async () => {
state.servicesYaml = [
{
Group: [
{ WidgetOnly: { widget: { type: "docker", container: "widgetapp", server: "local" } } },
{
WithArray: {
container: "app",
server: "local",
widgets: [
{ type: "docker", container: "sidecar", server: "local" },
{ type: "docker", container: "elsewhere", server: "remote" },
],
},
},
],
},
];
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("local")).toEqual(new Set(["widgetapp", "app", "sidecar"]));
});
it("containersFromConfig ignores non docker widgets that carry a container field", async () => {
state.servicesYaml = [
{ Group: [{ App: { widget: { type: "portainer", container: "sneaky", server: "local" } } }] },
];
const mod = await import("./service-helpers");
expect(await mod.containersFromConfig("local")).toEqual(new Set());
});
it("homepageLabelValue strips the prefix and honors instance scoping", async () => {
const mod = await import("./service-helpers");
expect(mod.homepageLabelValue("com.docker.compose.project", "foo")).toBeNull();
expect(mod.homepageLabelValue("homepage.name", undefined)).toBe("name");
expect(mod.homepageLabelValue("homepage.instance.foo.name", "foo")).toBe("name");
expect(mod.homepageLabelValue("homepage.instance.bar.name", "foo")).toBeNull();
expect(mod.homepageLabelValue("homepage.instance.bar.name", undefined)).toBeNull();
});
it("hasHomepageLabels reports whether a container opts in for this instance", async () => {
const mod = await import("./service-helpers");
expect(mod.hasHomepageLabels(undefined, "foo")).toBe(false);
expect(mod.hasHomepageLabels({}, "foo")).toBe(false);
expect(mod.hasHomepageLabels({ "com.docker.compose.project": "x" }, "foo")).toBe(false);
expect(mod.hasHomepageLabels({ "homepage.name": "X" }, undefined)).toBe(true);
expect(mod.hasHomepageLabels({ "homepage.instance.foo.name": "X" }, "foo")).toBe(true);
expect(mod.hasHomepageLabels({ "homepage.instance.bar.name": "X" }, "foo")).toBe(false);
});
it("servicesFromDocker skips containers without labels instead of failing the whole server", async () => {
state.dockerYaml = { "docker-local": {} };
state.dockerContainersByServer["docker-local"] = [
{ Names: ["/nolabels"] },
{ Names: ["/labelled"], Labels: { "homepage.group": "G", "homepage.name": "Svc" } },
];
const mod = await import("./service-helpers");
const discovered = await mod.servicesFromDocker();
expect(discovered).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "G",
services: [expect.objectContaining({ name: "Svc", container: "labelled" })],
}),
]),
);
});
it("servicesFromDocker returns [] when docker.yaml is empty", async () => {
state.dockerYaml = null;
+104
View File
@@ -0,0 +1,104 @@
import Docker from "dockerode";
import { getSettings } from "utils/config/config";
import getDockerArguments from "utils/config/docker";
import { containersFromConfig, hasHomepageLabels } from "utils/config/service-helpers";
const HEALTH_STATES = ["healthy", "unhealthy", "starting"];
function statusEntry(state, health) {
return health ? { status: state, health } : { status: state };
}
// docker exposes health only as a filter on the list endpoint, never as a field
async function healthByContainerId(docker) {
const results = await Promise.all(
HEALTH_STATES.map((state) => docker.listContainers({ all: true, filters: { health: [state] } }).catch(() => [])),
);
const health = {};
results.forEach((containers, index) => {
if (!Array.isArray(containers)) return;
containers.forEach((container) => {
health[container.Id] = HEALTH_STATES[index];
});
});
return health;
}
export async function getDockerStatuses(server) {
const dockerArgs = getDockerArguments(server);
const docker = new Docker(dockerArgs.conn);
const [containers, health, configured] = await Promise.all([
docker.listContainers({ all: true }),
healthByContainerId(docker),
containersFromConfig(server),
]);
if (!Array.isArray(containers)) {
return { error: "query failed" };
}
const { instanceName } = getSettings();
const statuses = {};
const byId = {};
containers.forEach((container) => {
const info = statusEntry(container.State, health[container.Id]);
// keyed by id for every container so swarm tasks can resolve their local container
byId[container.Id] = info;
const labelled = hasHomepageLabels(container.Labels, instanceName);
container.Names.forEach((name) => {
const containerName = name.replace(/^\//, "");
if (labelled || configured.has(containerName)) statuses[containerName] = info;
});
});
if (!dockerArgs.swarm) {
return { statuses };
}
const [services, tasks] = await Promise.all([
docker.listServices().catch(() => []),
docker.listTasks({ filters: { "desired-state": ["running"] } }).catch(() => []),
]);
const localIds = new Set(containers.map((container) => container.Id));
const tasksByService = {};
tasks.forEach((task) => {
(tasksByService[task.ServiceID] ??= []).push(task);
});
services.forEach((service) => {
const name = service.Spec?.Name;
if (!name || statuses[name]) return;
if (!configured.has(name) && !hasHomepageLabels(service.Spec?.Labels, instanceName)) return;
const serviceTasks = tasksByService[service.ID] ?? [];
if (service.Spec.Mode?.Replicated) {
const replicas = parseInt(service.Spec.Mode.Replicated.Replicas, 10);
if (serviceTasks.length === replicas) {
statuses[name] = { status: `running ${serviceTasks.length}/${replicas}` };
} else if (serviceTasks.length > 0) {
statuses[name] = { status: `partial ${serviceTasks.length}/${replicas}` };
}
return;
}
const task =
serviceTasks.find((candidate) => localIds.has(candidate.Status?.ContainerStatus?.ContainerID)) ??
serviceTasks.at(0);
const containerId = task?.Status?.ContainerStatus?.ContainerID;
if (containerId && byId[containerId]) {
statuses[name] = byId[containerId];
} else if (task) {
statuses[name] = { status: task.Status.State };
}
});
return { statuses };
}