feat: docker widget (#2288)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com> Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com> Co-authored-by: homarr-crowdin[bot] <190541745+homarr-crowdin[bot]@users.noreply.github.com> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -1,90 +1,48 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
import { db, like, or } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
|
||||
import { DockerSingleton } from "@homarr/docker";
|
||||
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createCacheChannel } from "@homarr/redis";
|
||||
import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
|
||||
|
||||
import { dockerMiddleware } from "../../middlewares/docker";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
const dockerCache = createCacheChannel<{
|
||||
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
|
||||
}>("docker-containers", 5 * 60 * 1000);
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.concat(dockerMiddleware())
|
||||
.query(async () => {
|
||||
const result = await dockerCache
|
||||
.consumeAsync(async () => {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(
|
||||
// Return all the containers of all the instances into only one item
|
||||
dockerInstances.map(({ instance, host: key }) =>
|
||||
instance.listContainers({ all: true }).then((containers) =>
|
||||
containers.map((container) => ({
|
||||
...container,
|
||||
instance: key,
|
||||
})),
|
||||
),
|
||||
),
|
||||
).then((containers) => containers.flat());
|
||||
|
||||
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||
const dbIcons =
|
||||
likeQueries.length >= 1
|
||||
? await db.query.icons.findMany({
|
||||
where: or(...likeQueries),
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
containers: containers.map((container) => ({
|
||||
...container,
|
||||
iconUrl:
|
||||
dbIcons.find((icon) => {
|
||||
const extractedImage = extractImage(container);
|
||||
if (!extractedImage) return false;
|
||||
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
|
||||
})?.url ?? null,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
return {
|
||||
isError: true,
|
||||
error: error as unknown,
|
||||
};
|
||||
});
|
||||
|
||||
if ("isError" in result) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching the containers",
|
||||
cause: result.error,
|
||||
});
|
||||
}
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
const result = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
const { data, timestamp } = result;
|
||||
|
||||
return {
|
||||
containers: sanitizeContainers(data.containers),
|
||||
containers: data satisfies DockerContainer[],
|
||||
timestamp,
|
||||
};
|
||||
}),
|
||||
subscribeContainers: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.concat(dockerMiddleware())
|
||||
.subscription(() => {
|
||||
return observable<DockerContainer[]>((emit) => {
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
const unsubscribe = innerHandler.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}),
|
||||
invalidate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.concat(dockerMiddleware())
|
||||
.mutation(async () => {
|
||||
await dockerCache.invalidateAsync();
|
||||
return;
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
await innerHandler.invalidateAsync();
|
||||
}),
|
||||
startAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -98,7 +56,8 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
await dockerCache.invalidateAsync();
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
await innerHandler.invalidateAsync();
|
||||
}),
|
||||
stopAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -112,7 +71,8 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
await dockerCache.invalidateAsync();
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
await innerHandler.invalidateAsync();
|
||||
}),
|
||||
restartAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -126,7 +86,8 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
await dockerCache.invalidateAsync();
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
await innerHandler.invalidateAsync();
|
||||
}),
|
||||
removeAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -140,7 +101,8 @@ export const dockerRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
await dockerCache.invalidateAsync();
|
||||
const innerHandler = dockerContainersRequestHandler.handler({});
|
||||
await innerHandler.invalidateAsync();
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -180,20 +142,6 @@ interface DockerContainer {
|
||||
image: string;
|
||||
ports: Port[];
|
||||
iconUrl: string | null;
|
||||
}
|
||||
|
||||
function sanitizeContainers(
|
||||
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[],
|
||||
): DockerContainer[] {
|
||||
return containers.map((container) => {
|
||||
return {
|
||||
name: container.Names[0]?.split("/")[1] ?? "Unknown",
|
||||
id: container.Id,
|
||||
instance: container.instance,
|
||||
state: container.State as ContainerState,
|
||||
image: container.Image,
|
||||
ports: container.Ports,
|
||||
iconUrl: container.iconUrl,
|
||||
};
|
||||
});
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,26 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
|
||||
import type { RouterInputs } from "../../..";
|
||||
import { dockerRouter } from "../../docker/docker-router";
|
||||
|
||||
// Mock the auth module to return an empty session
|
||||
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||
vi.mock("@homarr/request-handler/docker", () => ({
|
||||
dockerContainersRequestHandler: {
|
||||
handler: () => ({
|
||||
getCachedOrUpdatedDataAsync: async () => {
|
||||
return await Promise.resolve({ containers: [] });
|
||||
},
|
||||
invalidateAsync: async () => {
|
||||
return await Promise.resolve();
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock("@homarr/redis", () => ({
|
||||
createCacheChannel: () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -22,6 +34,7 @@ vi.mock("@homarr/redis", () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
invalidateAsync: async () => {},
|
||||
}),
|
||||
createWidgetOptionsChannel: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/docker/env", () => ({
|
||||
@@ -46,6 +59,7 @@ const validInputs: {
|
||||
[key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key];
|
||||
} = {
|
||||
getContainers: undefined,
|
||||
subscribeContainers: undefined,
|
||||
startAll: { ids: ["1"] },
|
||||
stopAll: { ids: ["1"] },
|
||||
restartAll: { ids: ["1"] },
|
||||
|
||||
Reference in New Issue
Block a user