Normalize docker agent version handling
This commit is contained in:
parent
7733d4b870
commit
f2acdd59af
20 changed files with 1600 additions and 1492 deletions
|
|
@ -45,12 +45,9 @@ export function MetricBar(props: MetricBarProps) {
|
|||
});
|
||||
|
||||
return (
|
||||
<div class="metric-text">
|
||||
<div class="relative min-w-[96px] w-full h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
|
||||
<div
|
||||
class={`absolute top-0 left-0 h-full ${progressColorClass()}`}
|
||||
style={{ width: `${width()}%` }}
|
||||
/>
|
||||
<div class="metric-text w-full">
|
||||
<div class="relative w-full h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
|
||||
<div class={`absolute top-0 left-0 h-full ${progressColorClass()}`} style={{ width: `${width()}%` }} />
|
||||
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-gray-800 dark:text-gray-100 leading-none">
|
||||
<span class="flex items-center gap-1 whitespace-nowrap px-0.5">
|
||||
<span>{props.label}</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { DockerHost } from '@/types/api';
|
|||
import { Card } from '@/components/shared/Card';
|
||||
import { MetricBar } from '@/components/Dashboard/MetricBar';
|
||||
import { renderDockerStatusBadge } from './DockerStatusBadge';
|
||||
import { formatUptime } from '@/utils/format';
|
||||
import { formatPercent, formatUptime } from '@/utils/format';
|
||||
import { ScrollableTable } from '@/components/shared/ScrollableTable';
|
||||
|
||||
export interface DockerHostSummary {
|
||||
|
|
@ -11,6 +11,8 @@ export interface DockerHostSummary {
|
|||
cpuPercent: number;
|
||||
memoryPercent: number;
|
||||
memoryLabel?: string;
|
||||
diskPercent: number;
|
||||
diskLabel?: string;
|
||||
runningPercent: number;
|
||||
runningCount: number;
|
||||
totalCount: number;
|
||||
|
|
@ -25,7 +27,7 @@ interface DockerHostSummaryTableProps {
|
|||
onSelect: (hostId: string) => void;
|
||||
}
|
||||
|
||||
type SortKey = 'name' | 'uptime' | 'cpu' | 'memory' | 'running' | 'lastSeen' | 'agent';
|
||||
type SortKey = 'name' | 'uptime' | 'cpu' | 'memory' | 'disk' | 'running' | 'lastSeen' | 'agent';
|
||||
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
|
|
@ -52,6 +54,19 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
return lastSeen;
|
||||
};
|
||||
|
||||
const runningStatusClass = (summary: DockerHostSummary) => {
|
||||
if (!summary.totalCount || summary.totalCount <= 0) {
|
||||
return 'text-gray-400 dark:text-gray-500';
|
||||
}
|
||||
if (summary.runningPercent >= 99) {
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
}
|
||||
if (summary.runningPercent >= 70) {
|
||||
return 'text-yellow-600 dark:text-yellow-400';
|
||||
}
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
};
|
||||
|
||||
const sortedSummaries = createMemo(() => {
|
||||
const list = [...props.summaries()];
|
||||
const key = sortKey();
|
||||
|
|
@ -75,6 +90,9 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
case 'memory':
|
||||
value = a.memoryPercent - b.memoryPercent;
|
||||
break;
|
||||
case 'disk':
|
||||
value = a.diskPercent - b.diskPercent;
|
||||
break;
|
||||
case 'running':
|
||||
value = a.runningPercent - b.runningPercent;
|
||||
break;
|
||||
|
|
@ -129,21 +147,27 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
>
|
||||
Host {renderSortIndicator('name')}
|
||||
</th>
|
||||
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">
|
||||
<th class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('cpu')}
|
||||
>
|
||||
CPU {renderSortIndicator('cpu')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('memory')}
|
||||
>
|
||||
Memory {renderSortIndicator('memory')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-[140px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('disk')}
|
||||
>
|
||||
Disk {renderSortIndicator('disk')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('running')}
|
||||
|
|
@ -151,19 +175,19 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
Containers {renderSortIndicator('running')}
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
class="hidden px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
onClick={() => handleSort('uptime')}
|
||||
>
|
||||
Uptime {renderSortIndicator('uptime')}
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
class="hidden px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
onClick={() => handleSort('lastSeen')}
|
||||
>
|
||||
Last Update {renderSortIndicator('lastSeen')}
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
class="hidden px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
|
||||
onClick={() => handleSort('agent')}
|
||||
>
|
||||
Agent {renderSortIndicator('agent')}
|
||||
|
|
@ -176,7 +200,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
const selected = props.selectedHostId() === summary.host.id;
|
||||
const online = isHostOnline(summary.host);
|
||||
const uptimeLabel = summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—';
|
||||
const agentLabel = summary.host.agentVersion ? `v${summary.host.agentVersion}` : '—';
|
||||
const agentLabel = summary.host.agentVersion ? summary.host.agentVersion : '—';
|
||||
|
||||
const rowStyle = () => {
|
||||
const styles: Record<string, string> = {};
|
||||
|
|
@ -217,7 +241,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
style={rowStyle()}
|
||||
onClick={() => props.onSelect(summary.host.id)}
|
||||
>
|
||||
<td class="pr-2 py-1 pl-3 align-top">
|
||||
<td class="pr-2 py-1 pl-3 align-middle">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span class="font-medium text-[11px] text-gray-900 dark:text-gray-100">
|
||||
{summary.host.displayName}
|
||||
|
|
@ -274,80 +298,107 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
|||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 py-1 align-top">
|
||||
{renderDockerStatusBadge(summary.host.status)}
|
||||
</td>
|
||||
<td class="px-2 py-1 align-top">
|
||||
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}>
|
||||
<MetricBar
|
||||
value={summary.cpuPercent}
|
||||
label={`${summary.cpuPercent.toFixed(1)}%`}
|
||||
type="cpu"
|
||||
/>
|
||||
</Show>
|
||||
</td>
|
||||
<td class="px-2 py-1 align-top">
|
||||
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}>
|
||||
<MetricBar
|
||||
value={summary.memoryPercent}
|
||||
label={`${summary.memoryPercent.toFixed(1)}%`}
|
||||
sublabel={summary.memoryLabel}
|
||||
type="memory"
|
||||
/>
|
||||
</Show>
|
||||
</td>
|
||||
<td class="px-2 py-1 align-top">
|
||||
<div class="flex justify-start sm:justify-center">
|
||||
<MetricBar
|
||||
value={summary.runningPercent}
|
||||
label={`${summary.runningCount}/${summary.totalCount}`}
|
||||
type="generic"
|
||||
/>
|
||||
<td class="px-2 py-1 align-middle">
|
||||
<div class="flex justify-center items-center h-full w-full max-w-[180px]">
|
||||
{renderDockerStatusBadge(summary.host.status)}
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-top whitespace-nowrap sm:table-cell">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{uptimeLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-top sm:table-cell">
|
||||
<div class="text-sm text-gray-900 dark:text-gray-100">
|
||||
{summary.lastSeenRelative}
|
||||
<td class="px-2 py-1 align-middle">
|
||||
<div class="flex justify-center items-center h-full w-full max-w-[180px]">
|
||||
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}>
|
||||
<MetricBar value={summary.cpuPercent} label={formatPercent(summary.cpuPercent)} type="cpu" />
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={summary.lastSeenAbsolute}>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{summary.lastSeenAbsolute}
|
||||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-top sm:table-cell">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<Show when={summary.host.agentVersion}>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<td class="px-2 py-1 align-middle">
|
||||
<div class="flex justify-center items-center h-full w-full max-w-[180px]">
|
||||
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}>
|
||||
<MetricBar
|
||||
value={summary.memoryPercent}
|
||||
label={formatPercent(summary.memoryPercent)}
|
||||
sublabel={summary.memoryLabel}
|
||||
type="memory"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 py-1 align-middle">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<Show when={summary.diskLabel} fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}>
|
||||
<MetricBar
|
||||
value={summary.diskPercent}
|
||||
label={formatPercent(summary.diskPercent)}
|
||||
sublabel={summary.diskLabel}
|
||||
type="disk"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 py-1 align-middle">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<Show
|
||||
when={summary.totalCount > 0}
|
||||
fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}
|
||||
>
|
||||
<span
|
||||
class={`text-xs font-medium whitespace-nowrap ${runningStatusClass(summary)}`}
|
||||
title={`${formatPercent(summary.runningPercent)} running`}
|
||||
>
|
||||
{summary.runningCount}/{summary.totalCount}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-middle whitespace-nowrap sm:table-cell">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{uptimeLabel}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-middle sm:table-cell">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<Show
|
||||
when={summary.lastSeenRelative}
|
||||
fallback={<span class="text-xs text-gray-400 dark:text-gray-500">—</span>}
|
||||
>
|
||||
{(relative) => (
|
||||
<span
|
||||
class="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap"
|
||||
title={summary.lastSeenAbsolute || undefined}
|
||||
>
|
||||
{relative()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden px-2 py-1 align-middle sm:table-cell">
|
||||
<div class="flex items-center justify-center gap-2 whitespace-nowrap text-[10px] h-full">
|
||||
<Show
|
||||
when={summary.host.agentVersion}
|
||||
fallback={<span class="text-gray-400 dark:text-gray-500 text-xs">—</span>}
|
||||
>
|
||||
{(version) => (
|
||||
<span
|
||||
class={
|
||||
agentOutdated
|
||||
? 'text-[10px] px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 font-medium inline-block w-fit'
|
||||
: 'text-[10px] px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-medium inline-block w-fit'
|
||||
? 'px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 font-medium'
|
||||
: 'px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-medium'
|
||||
}
|
||||
title={
|
||||
title={`${
|
||||
agentOutdated
|
||||
? 'Outdated - Update recommended'
|
||||
: 'Up to date - Auto-update enabled'
|
||||
}
|
||||
? 'Agent is outdated on this host'
|
||||
: 'Agent is up to date'
|
||||
}${summary.host.intervalSeconds ? `\nReporting interval: ${summary.host.intervalSeconds}s` : ''}`}
|
||||
>
|
||||
v{summary.host.agentVersion}
|
||||
v{version()}
|
||||
</span>
|
||||
<Show when={agentOutdated}>
|
||||
<span class="text-[9px] text-yellow-600 dark:text-yellow-500 font-medium">
|
||||
⚠ Update available
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={summary.host.intervalSeconds}>
|
||||
<span class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{summary.host.intervalSeconds}s
|
||||
<Show when={agentOutdated}>
|
||||
<span class="text-[10px] text-yellow-600 dark:text-yellow-500 font-medium" title="Update recommended">
|
||||
⚠
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { Show, createMemo, createSignal, onMount, onCleanup } from 'solid-js';
|
||||
import { Show, createMemo, createSignal, createEffect, onMount, onCleanup } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import type { DockerHost } from '@/types/api';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { DockerFilter } from './DockerFilter';
|
||||
import { DockerSummaryStatsBar } from './DockerSummaryStats';
|
||||
import { DockerHostSummaryTable, type DockerHostSummary } from './DockerHostSummaryTable';
|
||||
import { DockerUnifiedTable } from './DockerUnifiedTable';
|
||||
import { useWebSocket } from '@/App';
|
||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||
import { formatBytes, formatRelativeTime } from '@/utils/format';
|
||||
|
||||
interface DockerHostsProps {
|
||||
hosts: DockerHost[];
|
||||
activeAlerts?: Record<string, unknown> | any;
|
||||
}
|
||||
|
||||
type StatsFilter = { type: 'host-status' | 'container-state' | 'service-health'; value: string } | null;
|
||||
|
||||
export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const { initialDataReceived, reconnecting, connected } = useWebSocket();
|
||||
|
|
@ -40,8 +39,72 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
|
||||
const [search, setSearch] = createSignal('');
|
||||
const debouncedSearch = useDebouncedValue(search, 250);
|
||||
const [selectedHostId, setSelectedHostId] = createSignal<string | null>(null);
|
||||
|
||||
const [statsFilter, setStatsFilter] = createSignal<StatsFilter>(null);
|
||||
const clampPercent = (value: number | undefined | null) => {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) return 0;
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
if (value < 0) return 0;
|
||||
if (value > 100) return 100;
|
||||
return value;
|
||||
};
|
||||
|
||||
const hostSummaries = createMemo<DockerHostSummary[]>(() => {
|
||||
return sortedHosts().map((host) => {
|
||||
const totalContainers = host.containers?.length ?? 0;
|
||||
const runningContainers =
|
||||
host.containers?.filter((container) => container.state?.toLowerCase() === 'running').length ?? 0;
|
||||
const runningPercent = totalContainers > 0 ? clampPercent((runningContainers / totalContainers) * 100) : 0;
|
||||
|
||||
const cpuPercent = clampPercent(host.cpuUsagePercent ?? 0);
|
||||
|
||||
const memoryUsed = host.memory?.used ?? 0;
|
||||
const memoryTotal = host.memory?.total ?? host.totalMemoryBytes ?? 0;
|
||||
const memoryPercent = host.memory?.usage
|
||||
? clampPercent(host.memory.usage)
|
||||
: memoryTotal > 0
|
||||
? clampPercent((memoryUsed / memoryTotal) * 100)
|
||||
: 0;
|
||||
const memoryLabel =
|
||||
memoryTotal > 0 ? `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}` : undefined;
|
||||
|
||||
let diskPercent = 0;
|
||||
let diskLabel: string | undefined;
|
||||
if (host.disks && host.disks.length > 0) {
|
||||
const totals = host.disks.reduce(
|
||||
(acc, disk) => {
|
||||
acc.used += disk.used ?? 0;
|
||||
acc.total += disk.total ?? 0;
|
||||
return acc;
|
||||
},
|
||||
{ used: 0, total: 0 },
|
||||
);
|
||||
if (totals.total > 0) {
|
||||
diskPercent = clampPercent((totals.used / totals.total) * 100);
|
||||
diskLabel = `${formatBytes(totals.used)} / ${formatBytes(totals.total)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const uptimeSeconds = host.uptimeSeconds ?? 0;
|
||||
const lastSeenRelative = host.lastSeen ? formatRelativeTime(host.lastSeen) : '—';
|
||||
const lastSeenAbsolute = host.lastSeen ? new Date(host.lastSeen).toLocaleString() : '';
|
||||
|
||||
return {
|
||||
host,
|
||||
cpuPercent,
|
||||
memoryPercent,
|
||||
memoryLabel,
|
||||
diskPercent,
|
||||
diskLabel,
|
||||
runningPercent,
|
||||
runningCount: runningContainers,
|
||||
totalCount: totalContainers,
|
||||
uptimeSeconds,
|
||||
lastSeenRelative,
|
||||
lastSeenAbsolute,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let searchInputRef: HTMLInputElement | undefined;
|
||||
|
||||
|
|
@ -52,12 +115,6 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (event.key === 'Escape' && statsFilter()) {
|
||||
event.preventDefault();
|
||||
setStatsFilter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -76,18 +133,18 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
onMount(() => document.addEventListener('keydown', handleKeyDown));
|
||||
onCleanup(() => document.removeEventListener('keydown', handleKeyDown));
|
||||
|
||||
const handleStatsFilterChange = (filter: StatsFilter) => {
|
||||
if (!filter) {
|
||||
setStatsFilter(null);
|
||||
createEffect(() => {
|
||||
const hostId = selectedHostId();
|
||||
if (!hostId) {
|
||||
return;
|
||||
}
|
||||
if (!sortedHosts().some((host) => host.id === hostId)) {
|
||||
setSelectedHostId(null);
|
||||
}
|
||||
});
|
||||
|
||||
setStatsFilter((current) => {
|
||||
if (current && current.type === filter.type && current.value === filter.value) {
|
||||
return null;
|
||||
}
|
||||
return filter;
|
||||
});
|
||||
const handleHostSelect = (hostId: string) => {
|
||||
setSelectedHostId((current) => (current === hostId ? null : hostId));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -127,25 +184,25 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
setSearch={setSearch}
|
||||
onReset={() => {
|
||||
setSearch('');
|
||||
setStatsFilter(null);
|
||||
setSelectedHostId(null);
|
||||
}}
|
||||
searchInputRef={(el) => {
|
||||
searchInputRef = el;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Card padding="lg">
|
||||
<DockerSummaryStatsBar
|
||||
hosts={sortedHosts()}
|
||||
onFilterChange={handleStatsFilterChange}
|
||||
activeFilter={statsFilter()}
|
||||
<Show when={hostSummaries().length > 0}>
|
||||
<DockerHostSummaryTable
|
||||
summaries={hostSummaries}
|
||||
selectedHostId={selectedHostId}
|
||||
onSelect={handleHostSelect}
|
||||
/>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<DockerUnifiedTable
|
||||
hosts={sortedHosts()}
|
||||
searchTerm={debouncedSearch()}
|
||||
statsFilter={statsFilter()}
|
||||
selectedHostId={selectedHostId}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, For, Show, createMemo, createSignal } from 'solid-js';
|
||||
import type { Node, VM, Container, Storage, PBSInstance } from '@/types/api';
|
||||
import { formatBytes, formatUptime } from '@/utils/format';
|
||||
import { formatBytes, formatPercent, formatUptime } from '@/utils/format';
|
||||
import { MetricBar } from '@/components/Dashboard/MetricBar';
|
||||
import { useWebSocket } from '@/App';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
|
|
@ -563,7 +563,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
>
|
||||
<MetricBar
|
||||
value={cpuPercentValue ?? 0}
|
||||
label={`${cpuPercentValue}%`}
|
||||
label={formatPercent(cpuPercentValue ?? 0)}
|
||||
sublabel={
|
||||
isPVE && node!.cpuInfo?.cores
|
||||
? `${node!.cpuInfo.cores} cores`
|
||||
|
|
@ -580,7 +580,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
>
|
||||
<MetricBar
|
||||
value={memoryPercentValue ?? 0}
|
||||
label={`${memoryPercentValue}%`}
|
||||
label={formatPercent(memoryPercentValue ?? 0)}
|
||||
sublabel={
|
||||
isPVE && node!.memory
|
||||
? `${formatBytes(node!.memory.used)}/${formatBytes(node!.memory.total)}`
|
||||
|
|
@ -599,7 +599,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
>
|
||||
<MetricBar
|
||||
value={diskPercentValue ?? 0}
|
||||
label={`${diskPercentValue}%`}
|
||||
label={formatPercent(diskPercentValue ?? 0)}
|
||||
sublabel={diskSublabel}
|
||||
type="disk"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ export interface DockerHost {
|
|||
cpus: number;
|
||||
totalMemoryBytes: number;
|
||||
uptimeSeconds: number;
|
||||
cpuUsagePercent?: number;
|
||||
loadAverage?: number[];
|
||||
memory?: Memory;
|
||||
disks?: Disk[];
|
||||
networkInterfaces?: HostNetworkInterface[];
|
||||
status: string;
|
||||
lastSeen: number;
|
||||
intervalSeconds: number;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ export function formatSpeed(bytesPerSecond: number, decimals = 0): string {
|
|||
return `${formatBytes(bytesPerSecond, decimals)}/s`;
|
||||
}
|
||||
|
||||
export function formatPercent(value: number): string {
|
||||
if (!Number.isFinite(value)) return '0%';
|
||||
|
||||
const abs = Math.abs(value);
|
||||
|
||||
if (abs >= 10) {
|
||||
return `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
if (abs >= 1) {
|
||||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// Preserve tiny signals without overly long labels
|
||||
return `${value.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export function formatUptime(seconds: number): string {
|
||||
if (!seconds || seconds < 0) return '0s';
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostmetrics"
|
||||
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
|
@ -332,6 +333,13 @@ func (a *Agent) buildReport(ctx context.Context) (agentsdocker.Report, error) {
|
|||
|
||||
uptime := readSystemUptime()
|
||||
|
||||
metricsCtx, metricsCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
snapshot, err := hostmetrics.Collect(metricsCtx)
|
||||
metricsCancel()
|
||||
if err != nil {
|
||||
return agentsdocker.Report{}, fmt.Errorf("collect host metrics: %w", err)
|
||||
}
|
||||
|
||||
collectContainers := a.cfg.IncludeContainers
|
||||
if !collectContainers && (a.cfg.IncludeServices || a.cfg.IncludeTasks) && !info.Swarm.ControlAvailable {
|
||||
collectContainers = true
|
||||
|
|
@ -365,6 +373,11 @@ func (a *Agent) buildReport(ctx context.Context) (agentsdocker.Report, error) {
|
|||
TotalCPU: info.NCPU,
|
||||
TotalMemoryBytes: info.MemTotal,
|
||||
UptimeSeconds: uptime,
|
||||
CPUUsagePercent: safeFloat(snapshot.CPUUsagePercent),
|
||||
LoadAverage: append([]float64(nil), snapshot.LoadAverage...),
|
||||
Memory: snapshot.Memory,
|
||||
Disks: append([]agentsdocker.Disk(nil), snapshot.Disks...),
|
||||
Network: append([]agentsdocker.NetworkInterface(nil), snapshot.Network...),
|
||||
},
|
||||
Timestamp: time.Now().UTC(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ package dockeragent
|
|||
// overridden at build time via -ldflags for release artifacts. When building
|
||||
// from source without ldflags, it defaults to this development value.
|
||||
// Set to match deployed agents to prevent update loops in development.
|
||||
var Version = "v4.22.0-rc.6"
|
||||
var Version = "v4.30.0"
|
||||
|
|
|
|||
|
|
@ -9,18 +9,13 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostmetrics"
|
||||
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||
"github.com/rs/zerolog"
|
||||
gocpu "github.com/shirou/gopsutil/v4/cpu"
|
||||
godisk "github.com/shirou/gopsutil/v4/disk"
|
||||
gohost "github.com/shirou/gopsutil/v4/host"
|
||||
goload "github.com/shirou/gopsutil/v4/load"
|
||||
gomem "github.com/shirou/gopsutil/v4/mem"
|
||||
gonet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// Config controls the behaviour of the host agent.
|
||||
|
|
@ -220,29 +215,9 @@ func (a *Agent) buildReport(ctx context.Context) (agentshost.Report, error) {
|
|||
defer cancel()
|
||||
|
||||
uptime, _ := gohost.UptimeWithContext(collectCtx)
|
||||
loadAvg, _ := goload.AvgWithContext(collectCtx)
|
||||
cpuCount, _ := gocpu.CountsWithContext(collectCtx, true)
|
||||
cpuUsage, err := a.calculateCPUUsage(collectCtx)
|
||||
snapshot, err := hostmetrics.Collect(collectCtx)
|
||||
if err != nil {
|
||||
a.logger.Debug().Err(err).Msg("failed to compute cpu usage")
|
||||
}
|
||||
|
||||
memStats, err := gomem.VirtualMemoryWithContext(collectCtx)
|
||||
if err != nil {
|
||||
return agentshost.Report{}, fmt.Errorf("memory stats: %w", err)
|
||||
}
|
||||
|
||||
disks := a.collectDisks(collectCtx)
|
||||
network := a.collectNetwork(collectCtx)
|
||||
|
||||
var loadValues []float64
|
||||
if loadAvg != nil {
|
||||
loadValues = []float64{loadAvg.Load1, loadAvg.Load5, loadAvg.Load15}
|
||||
}
|
||||
|
||||
swapUsed := int64(0)
|
||||
if memStats.SwapTotal > memStats.SwapFree {
|
||||
swapUsed = int64(memStats.SwapTotal - memStats.SwapFree)
|
||||
return agentshost.Report{}, fmt.Errorf("collect metrics: %w", err)
|
||||
}
|
||||
|
||||
report := agentshost.Report{
|
||||
|
|
@ -263,23 +238,16 @@ func (a *Agent) buildReport(ctx context.Context) (agentshost.Report, error) {
|
|||
KernelVersion: a.kernelVersion,
|
||||
Architecture: a.architecture,
|
||||
CPUModel: "",
|
||||
CPUCount: cpuCount,
|
||||
CPUCount: snapshot.CPUCount,
|
||||
UptimeSeconds: int64(uptime),
|
||||
LoadAverage: loadValues,
|
||||
LoadAverage: append([]float64(nil), snapshot.LoadAverage...),
|
||||
},
|
||||
Metrics: agentshost.Metrics{
|
||||
CPUUsagePercent: cpuUsage,
|
||||
Memory: agentshost.MemoryMetric{
|
||||
TotalBytes: int64(memStats.Total),
|
||||
UsedBytes: int64(memStats.Used),
|
||||
FreeBytes: int64(memStats.Free),
|
||||
Usage: memStats.UsedPercent,
|
||||
SwapTotal: int64(memStats.SwapTotal),
|
||||
SwapUsed: swapUsed,
|
||||
},
|
||||
CPUUsagePercent: snapshot.CPUUsagePercent,
|
||||
Memory: snapshot.Memory,
|
||||
},
|
||||
Disks: disks,
|
||||
Network: network,
|
||||
Disks: append([]agentshost.Disk(nil), snapshot.Disks...),
|
||||
Network: append([]agentshost.NetworkInterface(nil), snapshot.Network...),
|
||||
Sensors: agentshost.Sensors{},
|
||||
Tags: append([]string(nil), a.cfg.Tags...),
|
||||
Timestamp: time.Now().UTC(),
|
||||
|
|
@ -288,123 +256,6 @@ func (a *Agent) buildReport(ctx context.Context) (agentshost.Report, error) {
|
|||
return report, nil
|
||||
}
|
||||
|
||||
func (a *Agent) calculateCPUUsage(ctx context.Context) (float64, error) {
|
||||
// Use Percent() with a 1 second measurement interval for cross-platform compatibility
|
||||
// This works reliably on macOS ARM64 where Times() is not implemented
|
||||
percentages, err := gocpu.PercentWithContext(ctx, time.Second, false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(percentages) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
usage := percentages[0]
|
||||
if usage < 0 {
|
||||
usage = 0
|
||||
}
|
||||
if usage > 100 {
|
||||
usage = 100
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func (a *Agent) collectDisks(ctx context.Context) []agentshost.Disk {
|
||||
partitions, err := godisk.PartitionsWithContext(ctx, true)
|
||||
if err != nil {
|
||||
a.logger.Debug().Err(err).Msg("failed to fetch disk partitions")
|
||||
return nil
|
||||
}
|
||||
|
||||
disks := make([]agentshost.Disk, 0, len(partitions))
|
||||
seen := make(map[string]struct{}, len(partitions))
|
||||
|
||||
for _, part := range partitions {
|
||||
if part.Mountpoint == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[part.Mountpoint]; ok {
|
||||
continue
|
||||
}
|
||||
seen[part.Mountpoint] = struct{}{}
|
||||
|
||||
usage, err := godisk.UsageWithContext(ctx, part.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if usage.Total == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
disks = append(disks, agentshost.Disk{
|
||||
Device: part.Device,
|
||||
Mountpoint: part.Mountpoint,
|
||||
Filesystem: part.Fstype,
|
||||
Type: part.Fstype,
|
||||
TotalBytes: int64(usage.Total),
|
||||
UsedBytes: int64(usage.Used),
|
||||
FreeBytes: int64(usage.Free),
|
||||
Usage: usage.UsedPercent,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool { return disks[i].Mountpoint < disks[j].Mountpoint })
|
||||
return disks
|
||||
}
|
||||
|
||||
func (a *Agent) collectNetwork(ctx context.Context) []agentshost.NetworkInterface {
|
||||
ifaces, err := gonet.InterfacesWithContext(ctx)
|
||||
if err != nil {
|
||||
a.logger.Debug().Err(err).Msg("failed to fetch network interfaces")
|
||||
return nil
|
||||
}
|
||||
|
||||
ioCounters, err := gonet.IOCountersWithContext(ctx, true)
|
||||
if err != nil {
|
||||
a.logger.Debug().Err(err).Msg("failed to fetch network counters")
|
||||
}
|
||||
ioMap := make(map[string]gonet.IOCountersStat, len(ioCounters))
|
||||
for _, stat := range ioCounters {
|
||||
ioMap[stat.Name] = stat
|
||||
}
|
||||
|
||||
interfaces := make([]agentshost.NetworkInterface, 0, len(ifaces))
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if len(iface.Addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
if isLoopback(iface.Flags) {
|
||||
continue
|
||||
}
|
||||
|
||||
addresses := make([]string, 0, len(iface.Addrs))
|
||||
for _, addr := range iface.Addrs {
|
||||
if addr.Addr != "" {
|
||||
addresses = append(addresses, addr.Addr)
|
||||
}
|
||||
}
|
||||
if len(addresses) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
counter := ioMap[iface.Name]
|
||||
ifaceEntry := agentshost.NetworkInterface{
|
||||
Name: iface.Name,
|
||||
MAC: iface.HardwareAddr,
|
||||
Addresses: addresses,
|
||||
RXBytes: counter.BytesRecv,
|
||||
TXBytes: counter.BytesSent,
|
||||
}
|
||||
|
||||
interfaces = append(interfaces, ifaceEntry)
|
||||
}
|
||||
|
||||
sort.Slice(interfaces, func(i, j int) bool { return interfaces[i].Name < interfaces[j].Name })
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func (a *Agent) sendReport(ctx context.Context, report agentshost.Report) error {
|
||||
payload, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
|
|
|
|||
191
internal/hostmetrics/collector.go
Normal file
191
internal/hostmetrics/collector.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package hostmetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||
gocpu "github.com/shirou/gopsutil/v4/cpu"
|
||||
godisk "github.com/shirou/gopsutil/v4/disk"
|
||||
goload "github.com/shirou/gopsutil/v4/load"
|
||||
gomem "github.com/shirou/gopsutil/v4/mem"
|
||||
gonet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// Snapshot represents a host resource utilisation sample.
|
||||
type Snapshot struct {
|
||||
CPUUsagePercent float64
|
||||
CPUCount int
|
||||
LoadAverage []float64
|
||||
Memory agentshost.MemoryMetric
|
||||
Disks []agentshost.Disk
|
||||
Network []agentshost.NetworkInterface
|
||||
}
|
||||
|
||||
// Collect gathers a point-in-time snapshot of host resource utilisation.
|
||||
func Collect(ctx context.Context) (Snapshot, error) {
|
||||
collectCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var snapshot Snapshot
|
||||
|
||||
if cpuCount, err := gocpu.CountsWithContext(collectCtx, true); err == nil {
|
||||
snapshot.CPUCount = cpuCount
|
||||
}
|
||||
|
||||
if cpuUsage, err := collectCPUUsage(collectCtx); err == nil {
|
||||
snapshot.CPUUsagePercent = cpuUsage
|
||||
}
|
||||
|
||||
if loadAvg, err := goload.AvgWithContext(collectCtx); err == nil && loadAvg != nil {
|
||||
snapshot.LoadAverage = []float64{loadAvg.Load1, loadAvg.Load5, loadAvg.Load15}
|
||||
}
|
||||
|
||||
memStats, err := gomem.VirtualMemoryWithContext(collectCtx)
|
||||
if err != nil {
|
||||
return Snapshot{}, fmt.Errorf("memory stats: %w", err)
|
||||
}
|
||||
|
||||
swapUsed := int64(0)
|
||||
if memStats.SwapTotal > memStats.SwapFree {
|
||||
swapUsed = int64(memStats.SwapTotal - memStats.SwapFree)
|
||||
}
|
||||
|
||||
snapshot.Memory = agentshost.MemoryMetric{
|
||||
TotalBytes: int64(memStats.Total),
|
||||
UsedBytes: int64(memStats.Used),
|
||||
FreeBytes: int64(memStats.Free),
|
||||
Usage: memStats.UsedPercent,
|
||||
SwapTotal: int64(memStats.SwapTotal),
|
||||
SwapUsed: swapUsed,
|
||||
}
|
||||
|
||||
snapshot.Disks = collectDisks(collectCtx)
|
||||
snapshot.Network = collectNetwork(collectCtx)
|
||||
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func collectCPUUsage(ctx context.Context) (float64, error) {
|
||||
percentages, err := gocpu.PercentWithContext(ctx, time.Second, false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(percentages) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
usage := percentages[0]
|
||||
if usage < 0 {
|
||||
usage = 0
|
||||
}
|
||||
if usage > 100 {
|
||||
usage = 100
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func collectDisks(ctx context.Context) []agentshost.Disk {
|
||||
partitions, err := godisk.PartitionsWithContext(ctx, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
disks := make([]agentshost.Disk, 0, len(partitions))
|
||||
seen := make(map[string]struct{}, len(partitions))
|
||||
|
||||
for _, part := range partitions {
|
||||
if part.Mountpoint == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[part.Mountpoint]; ok {
|
||||
continue
|
||||
}
|
||||
seen[part.Mountpoint] = struct{}{}
|
||||
|
||||
usage, err := godisk.UsageWithContext(ctx, part.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if usage.Total == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
disks = append(disks, agentshost.Disk{
|
||||
Device: part.Device,
|
||||
Mountpoint: part.Mountpoint,
|
||||
Filesystem: part.Fstype,
|
||||
Type: part.Fstype,
|
||||
TotalBytes: int64(usage.Total),
|
||||
UsedBytes: int64(usage.Used),
|
||||
FreeBytes: int64(usage.Free),
|
||||
Usage: usage.UsedPercent,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(disks, func(i, j int) bool { return disks[i].Mountpoint < disks[j].Mountpoint })
|
||||
return disks
|
||||
}
|
||||
|
||||
func collectNetwork(ctx context.Context) []agentshost.NetworkInterface {
|
||||
ifaces, err := gonet.InterfacesWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ioCounters, err := gonet.IOCountersWithContext(ctx, true)
|
||||
if err != nil {
|
||||
ioCounters = nil
|
||||
}
|
||||
ioMap := make(map[string]gonet.IOCountersStat, len(ioCounters))
|
||||
for _, stat := range ioCounters {
|
||||
ioMap[stat.Name] = stat
|
||||
}
|
||||
|
||||
interfaces := make([]agentshost.NetworkInterface, 0, len(ifaces))
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if len(iface.Addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
if isLoopback(iface.Flags) {
|
||||
continue
|
||||
}
|
||||
|
||||
addresses := make([]string, 0, len(iface.Addrs))
|
||||
for _, addr := range iface.Addrs {
|
||||
if addr.Addr != "" {
|
||||
addresses = append(addresses, addr.Addr)
|
||||
}
|
||||
}
|
||||
if len(addresses) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
counter := ioMap[iface.Name]
|
||||
ifaceEntry := agentshost.NetworkInterface{
|
||||
Name: iface.Name,
|
||||
MAC: iface.HardwareAddr,
|
||||
Addresses: addresses,
|
||||
RXBytes: counter.BytesRecv,
|
||||
TXBytes: counter.BytesSent,
|
||||
}
|
||||
|
||||
interfaces = append(interfaces, ifaceEntry)
|
||||
}
|
||||
|
||||
sort.Slice(interfaces, func(i, j int) bool { return interfaces[i].Name < interfaces[j].Name })
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func isLoopback(flags []string) bool {
|
||||
for _, flag := range flags {
|
||||
if strings.EqualFold(flag, "loopback") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1115,27 +1115,94 @@ func generateDockerHosts(config MockConfig) []models.DockerHost {
|
|||
services, tasks = generateDockerServicesAndTasks(hostname, containers, now)
|
||||
}
|
||||
|
||||
cpuUsage := clampFloat(10+rand.Float64()*70, 4, 98)
|
||||
loadAverage := []float64{
|
||||
clampFloat(rand.Float64()*float64(cpus), 0, float64(cpus)+1),
|
||||
clampFloat(rand.Float64()*float64(cpus), 0, float64(cpus)+1),
|
||||
clampFloat(rand.Float64()*float64(cpus), 0, float64(cpus)+1),
|
||||
}
|
||||
|
||||
memUsageRatio := clampFloat(0.3+rand.Float64()*0.55, 0.05, 0.98)
|
||||
usedMemoryBytes := int64(float64(totalMemoryBytes) * memUsageRatio)
|
||||
if usedMemoryBytes > totalMemoryBytes {
|
||||
usedMemoryBytes = totalMemoryBytes
|
||||
}
|
||||
freeMemoryBytes := totalMemoryBytes - usedMemoryBytes
|
||||
memory := models.Memory{
|
||||
Total: totalMemoryBytes,
|
||||
Used: usedMemoryBytes,
|
||||
Free: freeMemoryBytes,
|
||||
}
|
||||
if totalMemoryBytes > 0 {
|
||||
memory.Usage = clampFloat((float64(usedMemoryBytes)/float64(totalMemoryBytes))*100, 0, 100)
|
||||
}
|
||||
|
||||
diskTotal := int64((250 + rand.Intn(750)) * 1024 * 1024 * 1024) // 250-999 GB
|
||||
diskUsed := int64(float64(diskTotal) * clampFloat(0.35+rand.Float64()*0.5, 0.1, 0.97))
|
||||
if diskUsed > diskTotal {
|
||||
diskUsed = diskTotal
|
||||
}
|
||||
diskFree := diskTotal - diskUsed
|
||||
diskUsage := 0.0
|
||||
if diskTotal > 0 {
|
||||
diskUsage = clampFloat((float64(diskUsed)/float64(diskTotal))*100, 0, 100)
|
||||
}
|
||||
|
||||
disks := []models.Disk{
|
||||
{
|
||||
Total: diskTotal,
|
||||
Used: diskUsed,
|
||||
Free: diskFree,
|
||||
Usage: diskUsage,
|
||||
Mountpoint: "/",
|
||||
Type: "ext4",
|
||||
Device: "/dev/sda1",
|
||||
},
|
||||
}
|
||||
|
||||
networkInterfaces := []models.HostNetworkInterface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Addresses: []string{fmt.Sprintf("10.10.%d.%d/24", i%20, rand.Intn(200)+10)},
|
||||
RXBytes: uint64(rand.Int63n(5_000_000_000) + 500_000_000),
|
||||
TXBytes: uint64(rand.Int63n(4_000_000_000) + 400_000_000),
|
||||
},
|
||||
}
|
||||
if rand.Intn(4) == 0 {
|
||||
networkInterfaces = append(networkInterfaces, models.HostNetworkInterface{
|
||||
Name: "eth1",
|
||||
Addresses: []string{fmt.Sprintf("172.16.%d.%d/24", i%16, rand.Intn(200)+20)},
|
||||
RXBytes: uint64(rand.Int63n(2_000_000_000) + 200_000_000),
|
||||
TXBytes: uint64(rand.Int63n(1_500_000_000) + 150_000_000),
|
||||
})
|
||||
}
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: hostID,
|
||||
AgentID: fmt.Sprintf("agent-%s", randomHexString(6)),
|
||||
Hostname: hostname,
|
||||
DisplayName: humanizeHostDisplayName(hostname),
|
||||
MachineID: randomHexString(32),
|
||||
OS: dockerOperatingSystems[rand.Intn(len(dockerOperatingSystems))],
|
||||
KernelVersion: dockerKernelVersions[rand.Intn(len(dockerKernelVersions))],
|
||||
Architecture: dockerArchitectures[rand.Intn(len(dockerArchitectures))],
|
||||
DockerVersion: dockerVersions[rand.Intn(len(dockerVersions))],
|
||||
CPUs: cpus,
|
||||
TotalMemoryBytes: totalMemoryBytes,
|
||||
UptimeSeconds: uptime,
|
||||
Status: status,
|
||||
LastSeen: lastSeen,
|
||||
IntervalSeconds: interval,
|
||||
AgentVersion: agentVersion,
|
||||
Containers: containers,
|
||||
Services: services,
|
||||
Tasks: tasks,
|
||||
Swarm: swarmInfo,
|
||||
ID: hostID,
|
||||
AgentID: fmt.Sprintf("agent-%s", randomHexString(6)),
|
||||
Hostname: hostname,
|
||||
DisplayName: humanizeHostDisplayName(hostname),
|
||||
MachineID: randomHexString(32),
|
||||
OS: dockerOperatingSystems[rand.Intn(len(dockerOperatingSystems))],
|
||||
KernelVersion: dockerKernelVersions[rand.Intn(len(dockerKernelVersions))],
|
||||
Architecture: dockerArchitectures[rand.Intn(len(dockerArchitectures))],
|
||||
DockerVersion: dockerVersions[rand.Intn(len(dockerVersions))],
|
||||
CPUs: cpus,
|
||||
TotalMemoryBytes: totalMemoryBytes,
|
||||
UptimeSeconds: uptime,
|
||||
CPUUsage: cpuUsage,
|
||||
LoadAverage: loadAverage,
|
||||
Memory: memory,
|
||||
Disks: disks,
|
||||
NetworkInterfaces: networkInterfaces,
|
||||
Status: status,
|
||||
LastSeen: lastSeen,
|
||||
IntervalSeconds: interval,
|
||||
AgentVersion: agentVersion,
|
||||
Containers: containers,
|
||||
Services: services,
|
||||
Tasks: tasks,
|
||||
Swarm: swarmInfo,
|
||||
}
|
||||
|
||||
hosts = append(hosts, host)
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ func (d DockerHost) ToFrontend() DockerHostFrontend {
|
|||
CPUs: d.CPUs,
|
||||
TotalMemoryBytes: d.TotalMemoryBytes,
|
||||
UptimeSeconds: d.UptimeSeconds,
|
||||
CPUUsagePercent: d.CPUUsage,
|
||||
Status: d.Status,
|
||||
LastSeen: d.LastSeen.Unix() * 1000,
|
||||
IntervalSeconds: d.IntervalSeconds,
|
||||
|
|
@ -254,6 +255,24 @@ func (d DockerHost) ToFrontend() DockerHostFrontend {
|
|||
h.Swarm = &sw
|
||||
}
|
||||
|
||||
if len(d.LoadAverage) > 0 {
|
||||
h.LoadAverage = append([]float64(nil), d.LoadAverage...)
|
||||
}
|
||||
|
||||
if (d.Memory != Memory{}) {
|
||||
mem := d.Memory
|
||||
h.Memory = &mem
|
||||
}
|
||||
|
||||
if len(d.Disks) > 0 {
|
||||
h.Disks = append([]Disk(nil), d.Disks...)
|
||||
}
|
||||
|
||||
if len(d.NetworkInterfaces) > 0 {
|
||||
h.NetworkInterfaces = make([]HostNetworkInterface, len(d.NetworkInterfaces))
|
||||
copy(h.NetworkInterfaces, d.NetworkInterfaces)
|
||||
}
|
||||
|
||||
if d.Command != nil {
|
||||
h.Command = toDockerHostCommandFrontend(*d.Command)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,33 +190,38 @@ type HostSensorSummary struct {
|
|||
|
||||
// DockerHost represents a Docker host reporting metrics via the external agent.
|
||||
type DockerHost struct {
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agentId"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"displayName"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
CPUs int `json:"cpus"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
Status string `json:"status"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
Containers []DockerContainer `json:"containers"`
|
||||
Services []DockerService `json:"services,omitempty"`
|
||||
Tasks []DockerTask `json:"tasks,omitempty"`
|
||||
Swarm *DockerSwarmInfo `json:"swarm,omitempty"`
|
||||
TokenID string `json:"tokenId,omitempty"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
TokenHint string `json:"tokenHint,omitempty"`
|
||||
TokenLastUsedAt *time.Time `json:"tokenLastUsedAt,omitempty"`
|
||||
Hidden bool `json:"hidden"`
|
||||
PendingUninstall bool `json:"pendingUninstall"`
|
||||
Command *DockerHostCommandStatus `json:"command,omitempty"`
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agentId"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"displayName"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
CPUs int `json:"cpus"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
CPUUsage float64 `json:"cpuUsagePercent"`
|
||||
LoadAverage []float64 `json:"loadAverage,omitempty"`
|
||||
Memory Memory `json:"memory"`
|
||||
Disks []Disk `json:"disks,omitempty"`
|
||||
NetworkInterfaces []HostNetworkInterface `json:"networkInterfaces,omitempty"`
|
||||
Status string `json:"status"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
Containers []DockerContainer `json:"containers"`
|
||||
Services []DockerService `json:"services,omitempty"`
|
||||
Tasks []DockerTask `json:"tasks,omitempty"`
|
||||
Swarm *DockerSwarmInfo `json:"swarm,omitempty"`
|
||||
TokenID string `json:"tokenId,omitempty"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
TokenHint string `json:"tokenHint,omitempty"`
|
||||
TokenLastUsedAt *time.Time `json:"tokenLastUsedAt,omitempty"`
|
||||
Hidden bool `json:"hidden"`
|
||||
PendingUninstall bool `json:"pendingUninstall"`
|
||||
Command *DockerHostCommandStatus `json:"command,omitempty"`
|
||||
}
|
||||
|
||||
// DockerContainer represents the state of a Docker container on a monitored host.
|
||||
|
|
|
|||
|
|
@ -97,32 +97,37 @@ type ContainerFrontend struct {
|
|||
|
||||
// DockerHostFrontend represents a Docker host with frontend-friendly fields
|
||||
type DockerHostFrontend struct {
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agentId"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"displayName"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
CPUs int `json:"cpus"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"lastSeen"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
Containers []DockerContainerFrontend `json:"containers"`
|
||||
Services []DockerServiceFrontend `json:"services,omitempty"`
|
||||
Tasks []DockerTaskFrontend `json:"tasks,omitempty"`
|
||||
Swarm *DockerSwarmFrontend `json:"swarm,omitempty"`
|
||||
TokenID string `json:"tokenId,omitempty"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
TokenHint string `json:"tokenHint,omitempty"`
|
||||
TokenLastUsedAt *int64 `json:"tokenLastUsedAt,omitempty"`
|
||||
PendingUninstall bool `json:"pendingUninstall"`
|
||||
Command *DockerHostCommandFrontend `json:"command,omitempty"`
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agentId"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"displayName"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
CPUs int `json:"cpus"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
CPUUsagePercent float64 `json:"cpuUsagePercent"`
|
||||
LoadAverage []float64 `json:"loadAverage,omitempty"`
|
||||
Memory *Memory `json:"memory,omitempty"`
|
||||
Disks []Disk `json:"disks,omitempty"`
|
||||
NetworkInterfaces []HostNetworkInterface `json:"networkInterfaces,omitempty"`
|
||||
Status string `json:"status"`
|
||||
LastSeen int64 `json:"lastSeen"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
AgentVersion string `json:"agentVersion,omitempty"`
|
||||
Containers []DockerContainerFrontend `json:"containers"`
|
||||
Services []DockerServiceFrontend `json:"services,omitempty"`
|
||||
Tasks []DockerTaskFrontend `json:"tasks,omitempty"`
|
||||
Swarm *DockerSwarmFrontend `json:"swarm,omitempty"`
|
||||
TokenID string `json:"tokenId,omitempty"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
TokenHint string `json:"tokenHint,omitempty"`
|
||||
TokenLastUsedAt *int64 `json:"tokenLastUsedAt,omitempty"`
|
||||
PendingUninstall bool `json:"pendingUninstall"`
|
||||
Command *DockerHostCommandFrontend `json:"command,omitempty"`
|
||||
}
|
||||
|
||||
// DockerContainerFrontend represents a Docker container for the frontend
|
||||
|
|
|
|||
|
|
@ -646,6 +646,18 @@ func convertDockerTasks(tasks []agentsdocker.Task) []models.DockerTask {
|
|||
return result
|
||||
}
|
||||
|
||||
func normalizeAgentVersion(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" {
|
||||
return ""
|
||||
}
|
||||
version = strings.TrimLeft(version, "vV")
|
||||
if version == "" {
|
||||
return ""
|
||||
}
|
||||
return "v" + version
|
||||
}
|
||||
|
||||
func convertDockerSwarmInfo(info *agentsdocker.SwarmInfo) *models.DockerSwarmInfo {
|
||||
if info == nil {
|
||||
return nil
|
||||
|
|
@ -1390,27 +1402,80 @@ func (m *Monitor) ApplyDockerReport(report agentsdocker.Report, tokenRecord *con
|
|||
tasks := convertDockerTasks(report.Tasks)
|
||||
swarmInfo := convertDockerSwarmInfo(report.Host.Swarm)
|
||||
|
||||
loadAverage := make([]float64, 0, len(report.Host.LoadAverage))
|
||||
if len(report.Host.LoadAverage) > 0 {
|
||||
loadAverage = append(loadAverage, report.Host.LoadAverage...)
|
||||
}
|
||||
|
||||
var memory models.Memory
|
||||
if report.Host.Memory.TotalBytes > 0 || report.Host.Memory.UsedBytes > 0 {
|
||||
memory = models.Memory{
|
||||
Total: report.Host.Memory.TotalBytes,
|
||||
Used: report.Host.Memory.UsedBytes,
|
||||
Free: report.Host.Memory.FreeBytes,
|
||||
Usage: safeFloat(report.Host.Memory.Usage),
|
||||
SwapTotal: report.Host.Memory.SwapTotal,
|
||||
SwapUsed: report.Host.Memory.SwapUsed,
|
||||
}
|
||||
}
|
||||
|
||||
disks := make([]models.Disk, 0, len(report.Host.Disks))
|
||||
for _, disk := range report.Host.Disks {
|
||||
disks = append(disks, models.Disk{
|
||||
Total: disk.TotalBytes,
|
||||
Used: disk.UsedBytes,
|
||||
Free: disk.FreeBytes,
|
||||
Usage: safeFloat(disk.Usage),
|
||||
Mountpoint: disk.Mountpoint,
|
||||
Type: disk.Type,
|
||||
Device: disk.Device,
|
||||
})
|
||||
}
|
||||
|
||||
networkIfaces := make([]models.HostNetworkInterface, 0, len(report.Host.Network))
|
||||
for _, iface := range report.Host.Network {
|
||||
addresses := append([]string(nil), iface.Addresses...)
|
||||
networkIfaces = append(networkIfaces, models.HostNetworkInterface{
|
||||
Name: iface.Name,
|
||||
MAC: iface.MAC,
|
||||
Addresses: addresses,
|
||||
RXBytes: iface.RXBytes,
|
||||
TXBytes: iface.TXBytes,
|
||||
SpeedMbps: iface.SpeedMbps,
|
||||
})
|
||||
}
|
||||
|
||||
agentVersion := normalizeAgentVersion(report.Agent.Version)
|
||||
if agentVersion == "" && hasPrevious {
|
||||
agentVersion = normalizeAgentVersion(previous.AgentVersion)
|
||||
}
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: identifier,
|
||||
AgentID: agentID,
|
||||
Hostname: hostname,
|
||||
DisplayName: displayName,
|
||||
MachineID: strings.TrimSpace(report.Host.MachineID),
|
||||
OS: report.Host.OS,
|
||||
KernelVersion: report.Host.KernelVersion,
|
||||
Architecture: report.Host.Architecture,
|
||||
DockerVersion: report.Host.DockerVersion,
|
||||
CPUs: report.Host.TotalCPU,
|
||||
TotalMemoryBytes: report.Host.TotalMemoryBytes,
|
||||
UptimeSeconds: report.Host.UptimeSeconds,
|
||||
Status: "online",
|
||||
LastSeen: timestamp,
|
||||
IntervalSeconds: report.Agent.IntervalSeconds,
|
||||
AgentVersion: report.Agent.Version,
|
||||
Containers: containers,
|
||||
Services: services,
|
||||
Tasks: tasks,
|
||||
Swarm: swarmInfo,
|
||||
ID: identifier,
|
||||
AgentID: agentID,
|
||||
Hostname: hostname,
|
||||
DisplayName: displayName,
|
||||
MachineID: strings.TrimSpace(report.Host.MachineID),
|
||||
OS: report.Host.OS,
|
||||
KernelVersion: report.Host.KernelVersion,
|
||||
Architecture: report.Host.Architecture,
|
||||
DockerVersion: report.Host.DockerVersion,
|
||||
CPUs: report.Host.TotalCPU,
|
||||
TotalMemoryBytes: report.Host.TotalMemoryBytes,
|
||||
UptimeSeconds: report.Host.UptimeSeconds,
|
||||
CPUUsage: safeFloat(report.Host.CPUUsagePercent),
|
||||
LoadAverage: loadAverage,
|
||||
Memory: memory,
|
||||
Disks: disks,
|
||||
NetworkInterfaces: networkIfaces,
|
||||
Status: "online",
|
||||
LastSeen: timestamp,
|
||||
IntervalSeconds: report.Agent.IntervalSeconds,
|
||||
AgentVersion: agentVersion,
|
||||
Containers: containers,
|
||||
Services: services,
|
||||
Tasks: tasks,
|
||||
Swarm: swarmInfo,
|
||||
}
|
||||
|
||||
if tokenRecord != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
package dockeragent
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
hostagent "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||
)
|
||||
|
||||
type MemoryMetric = hostagent.MemoryMetric
|
||||
type Disk = hostagent.Disk
|
||||
type NetworkInterface = hostagent.NetworkInterface
|
||||
|
||||
// Report represents a single heartbeat from the Docker agent to Pulse.
|
||||
type Report struct {
|
||||
|
|
@ -21,17 +29,22 @@ type AgentInfo struct {
|
|||
|
||||
// HostInfo contains metadata about the Docker host where the agent runs.
|
||||
type HostInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Name string `json:"name,omitempty"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
TotalCPU int `json:"totalCpu,omitempty"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes,omitempty"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds,omitempty"`
|
||||
Swarm *SwarmInfo `json:"swarm,omitempty"`
|
||||
Hostname string `json:"hostname"`
|
||||
Name string `json:"name,omitempty"`
|
||||
MachineID string `json:"machineId,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
DockerVersion string `json:"dockerVersion,omitempty"`
|
||||
TotalCPU int `json:"totalCpu,omitempty"`
|
||||
TotalMemoryBytes int64 `json:"totalMemoryBytes,omitempty"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds,omitempty"`
|
||||
Swarm *SwarmInfo `json:"swarm,omitempty"`
|
||||
CPUUsagePercent float64 `json:"cpuUsagePercent,omitempty"`
|
||||
LoadAverage []float64 `json:"loadAverage,omitempty"`
|
||||
Memory MemoryMetric `json:"memory,omitempty"`
|
||||
Disks []Disk `json:"disks,omitempty"`
|
||||
Network []NetworkInterface `json:"network,omitempty"`
|
||||
}
|
||||
|
||||
// Container captures the runtime state for a Docker container at report time.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -175,30 +175,41 @@ FRONTEND_PORT=${PULSE_DEV_API_PORT}
|
|||
PORT=${PULSE_DEV_API_PORT}
|
||||
export FRONTEND_PORT PULSE_DEV_API_PORT PORT
|
||||
|
||||
# Set data directory based on mock mode to keep data isolated
|
||||
# Set data directory strategy for the backend
|
||||
if [[ ${PULSE_MOCK_MODE:-false} == "true" ]]; then
|
||||
export PULSE_DATA_DIR=/opt/pulse/tmp/mock-data
|
||||
# Ensure mock data directory exists
|
||||
mkdir -p "$PULSE_DATA_DIR"
|
||||
echo "[hot-dev] Mock mode: Using isolated data directory: ${PULSE_DATA_DIR}"
|
||||
else
|
||||
DEV_CONFIG_DIR="${ROOT_DIR}/tmp/dev-config"
|
||||
mkdir -p "$DEV_CONFIG_DIR"
|
||||
export PULSE_DATA_DIR="${DEV_CONFIG_DIR}"
|
||||
|
||||
DEV_KEY_FILE="${DEV_CONFIG_DIR}/.encryption.key"
|
||||
if [[ ! -f "${DEV_KEY_FILE}" ]]; then
|
||||
openssl rand -hex 32 > "${DEV_KEY_FILE}"
|
||||
chmod 600 "${DEV_KEY_FILE}"
|
||||
echo "[hot-dev] Generated dev encryption key at ${DEV_KEY_FILE}"
|
||||
if [[ -n ${PULSE_DATA_DIR:-} ]]; then
|
||||
echo "[hot-dev] Using preconfigured data directory: ${PULSE_DATA_DIR}"
|
||||
elif [[ ${HOT_DEV_USE_PROD_DATA:-false} == "true" ]]; then
|
||||
export PULSE_DATA_DIR=/etc/pulse
|
||||
echo "[hot-dev] HOT_DEV_USE_PROD_DATA=true – using production data directory: ${PULSE_DATA_DIR}"
|
||||
else
|
||||
DEV_CONFIG_DIR="${ROOT_DIR}/tmp/dev-config"
|
||||
mkdir -p "$DEV_CONFIG_DIR"
|
||||
export PULSE_DATA_DIR="${DEV_CONFIG_DIR}"
|
||||
echo "[hot-dev] Production mode: Using dev config directory: ${PULSE_DATA_DIR}"
|
||||
fi
|
||||
|
||||
# Only set the encryption key if the user hasn't provided one explicitly
|
||||
# Attempt to load encryption key automatically when not explicitly provided
|
||||
if [[ -z ${PULSE_ENCRYPTION_KEY:-} ]]; then
|
||||
export PULSE_ENCRYPTION_KEY="$(<"${DEV_KEY_FILE}")"
|
||||
if [[ -f "${PULSE_DATA_DIR}/.encryption.key" ]]; then
|
||||
export PULSE_ENCRYPTION_KEY="$(<"${PULSE_DATA_DIR}/.encryption.key")"
|
||||
echo "[hot-dev] Loaded encryption key from ${PULSE_DATA_DIR}/.encryption.key"
|
||||
elif [[ ${PULSE_DATA_DIR} == "${ROOT_DIR}/tmp/dev-config" ]]; then
|
||||
DEV_KEY_FILE="${PULSE_DATA_DIR}/.encryption.key"
|
||||
if [[ ! -f "${DEV_KEY_FILE}" ]]; then
|
||||
openssl rand -hex 32 > "${DEV_KEY_FILE}"
|
||||
chmod 600 "${DEV_KEY_FILE}"
|
||||
echo "[hot-dev] Generated dev encryption key at ${DEV_KEY_FILE}"
|
||||
fi
|
||||
export PULSE_ENCRYPTION_KEY="$(<"${DEV_KEY_FILE}")"
|
||||
else
|
||||
echo "[hot-dev] WARNING: No encryption key found for ${PULSE_DATA_DIR}. Encrypted config may fail to load."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[hot-dev] Production mode: Using dev config directory: ${PULSE_DATA_DIR}"
|
||||
fi
|
||||
|
||||
./pulse &
|
||||
|
|
|
|||
|
|
@ -877,6 +877,12 @@ if [[ "$UNINSTALL" != true ]]; then
|
|||
systemctl stop pulse-docker-agent
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if pgrep -f pulse-docker-agent > /dev/null 2>&1; then
|
||||
log_info "Stopping running agent process"
|
||||
pkill -f pulse-docker-agent 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue