Compare commits

..
Author SHA1 Message Date
ajnart a37e101bba fix(docker): normalize container CPU usage 2026-09-06 10:54:05 +02:00
8 changed files with 21 additions and 52 deletions

No files matched your search

+2 -2
View File
@@ -58,8 +58,8 @@ TURBO_TELEMETRY_DISABLED=1
# Enable kubernetes tool # Enable kubernetes tool
# ENABLE_KUBERNETES=true # ENABLE_KUBERNETES=true
# Enable the development-only mock integration. Never enable this in production. # Enable mock integration
# UNSAFE_ENABLE_MOCK_INTEGRATION=true UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Public URL contract. Values must be absolute HTTP(S) URLs without credentials, # Public URL contract. Values must be absolute HTTP(S) URLs without credentials,
# query strings, or fragments. Trailing slashes are normalized at startup. # query strings, or fragments. Trailing slashes are normalized at startup.
@@ -133,10 +133,9 @@ External redis is currently not supported with Proxmox Community Scripts
The advanced deployments environment variables should only be used if you know what you are doing. The advanced deployments environment variables should only be used if you know what you are doing.
::: :::
| Environment Variable | Description | Default | | Environment Variable | Description |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- | | ------------------------ | --------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable database migrations. For example, when migrations are managed separately in Helm deployments. | `false` | | `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
| `UNSAFE_ENABLE_MOCK_INTEGRATION` | Enable the development-only Mock integration. This exposes fabricated data and must not be used in production. | `false` |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts. trailing slashes when Homarr or Docusaurus starts.
@@ -47,9 +47,6 @@ export default async function IntegrationsNewPage(props: NewIntegrationPageProps
const tCreate = await getI18n("integration.page.create"); const tCreate = await getI18n("integration.page.create");
const currentKind = result.data; const currentKind = result.data;
if (currentKind === "mock" && !env.UNSAFE_ENABLE_MOCK_INTEGRATION) {
notFound();
}
return ( return (
<> <>
-2
View File
@@ -19,7 +19,6 @@ const publicHttpUrl = (variableName: string) =>
export const env = createEnv({ export const env = createEnv({
server: { server: {
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(), KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false),
DEMO_MODE: createBooleanSchema(false), DEMO_MODE: createBooleanSchema(false),
DEMO_READ_ONLY: createBooleanSchema(true), DEMO_READ_ONLY: createBooleanSchema(true),
HOMARR_WEBSITE_URL: publicHttpUrl("HOMARR_WEBSITE_URL").default("https://homarr.dev"), HOMARR_WEBSITE_URL: publicHttpUrl("HOMARR_WEBSITE_URL").default("https://homarr.dev"),
@@ -28,7 +27,6 @@ export const env = createEnv({
}, },
runtimeEnv: { runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
UNSAFE_ENABLE_MOCK_INTEGRATION: process.env.UNSAFE_ENABLE_MOCK_INTEGRATION,
DEMO_MODE: process.env.DEMO_MODE, DEMO_MODE: process.env.DEMO_MODE,
DEMO_READ_ONLY: process.env.DEMO_READ_ONLY, DEMO_READ_ONLY: process.env.DEMO_READ_ONLY,
HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL, HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL,
@@ -18,12 +18,7 @@ import {
integrationSecrets, integrationSecrets,
integrationUserPermissions, integrationUserPermissions,
} from "@homarr/db/schema"; } from "@homarr/db/schema";
import type { import type { GroupPermissionKey, IntegrationPermission, IntegrationSecretKind } from "@homarr/definitions";
GroupPermissionKey,
IntegrationKind,
IntegrationPermission,
IntegrationSecretKind,
} from "@homarr/definitions";
import { import {
getIntegrationKindsByCategory, getIntegrationKindsByCategory,
getPermissionsWithParents, getPermissionsWithParents,
@@ -44,7 +39,6 @@ import {
} from "@homarr/validation/integration"; } from "@homarr/validation/integration";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
import { env } from "../../env";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "./integration-access"; import { throwIfActionForbiddenAsync } from "./integration-access";
@@ -54,15 +48,6 @@ import { mapTestConnectionError } from "./map-test-connection-error";
const logger = createLogger({ module: "integrationRouter" }); const logger = createLogger({ module: "integrationRouter" });
const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch"); const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch");
const throwIfMockIntegrationIsDisabled = (kind: IntegrationKind) => {
if (kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION) return;
throw new TRPCError({
code: "FORBIDDEN",
message: "Mock integrations are disabled",
});
};
export const integrationRouter = createTRPCRouter({ export const integrationRouter = createTRPCRouter({
getKinds: publicProcedure getKinds: publicProcedure
.meta({ .meta({
@@ -73,14 +58,12 @@ export const integrationRouter = createTRPCRouter({
}, },
}) })
.query(() => { .query(() => {
return objectEntries(integrationDefs) return objectEntries(integrationDefs).map(([kind, def]) => ({
.filter(([kind]) => kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION) kind,
.map(([kind, def]) => ({ name: def.name,
kind, category: def.category,
name: def.name, requiredSecrets: def.secretKinds,
category: def.category, }));
requiredSecrets: def.secretKinds,
}));
}), }),
all: protectedProcedure all: protectedProcedure
.meta({ .meta({
@@ -246,8 +229,6 @@ export const integrationRouter = createTRPCRouter({
}) })
.input(integrationCreateSchema) .input(integrationCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
throwIfMockIntegrationIsDisabled(input.kind);
logger.info("Creating integration", { logger.info("Creating integration", {
name: input.name, name: input.name,
kind: input.kind, kind: input.kind,
@@ -321,13 +321,6 @@ export const onboardRouter = createTRPCRouter({
.requiresStep("setup") .requiresStep("setup")
.input(onboardingCompleteSetupSchema) .input(onboardingCompleteSetupSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (
!env.UNSAFE_ENABLE_MOCK_INTEGRATION &&
input.integrations.some((integration) => integration.kind === "mock")
) {
throw new TRPCError({ code: "FORBIDDEN", message: "Mock integrations are disabled" });
}
const boardList = await ctx.db.query.boards.findMany({ const boardList = await ctx.db.query.boards.findMany({
with: { with: {
sections: { with: { layouts: true } }, sections: { with: { layouts: true } },
+5 -4
View File
@@ -481,13 +481,14 @@ export function calculateCpuUsage(stats: ContainerStats): number {
return 0; return 0;
} }
const numberOfCpus = stats.cpu_stats.online_cpus; const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0);
const usage = stats.cpu_stats.system_cpu_usage; const systemDelta = (stats.cpu_stats.system_cpu_usage ?? 0) - (stats.precpu_stats?.system_cpu_usage ?? 0);
if (!usage || usage === 0) {
if (systemDelta <= 0 || cpuDelta < 0) {
return 0; return 0;
} }
return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; return (cpuDelta / systemDelta) * 100;
} }
export function calculateMemoryUsage(stats: ContainerStats): number { export function calculateMemoryUsage(stats: ContainerStats): number {
@@ -72,16 +72,16 @@ describe("calculateCpuUsage", () => {
const stats = createStats({ const stats = createStats({
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 2000 }, system_cpu_usage: 10000 }, cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 2000 }, system_cpu_usage: 10000 },
}); });
// (2000 / 10000) * 4 * 100 = 80 // (2000 / 10000) * 100 = 20
expect(calculateCpuUsage(stats)).toBe(80); expect(calculateCpuUsage(stats)).toBe(20);
}); });
test("should handle fractional CPU usage", () => { test("should handle fractional CPU usage", () => {
const stats = createStats({ const stats = createStats({
cpu_stats: { online_cpus: 2, cpu_usage: { total_usage: 500 }, system_cpu_usage: 100000 }, cpu_stats: { online_cpus: 2, cpu_usage: { total_usage: 500 }, system_cpu_usage: 100000 },
}); });
// (500 / 100000) * 2 * 100 = 1 // (500 / 100000) * 100 = 0.5
expect(calculateCpuUsage(stats)).toBe(1); expect(calculateCpuUsage(stats)).toBe(0.5);
}); });
}); });