mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Add host agent multi-platform binary distribution and improve host details UI
- Build host agent binaries for all platforms (linux/darwin/windows, amd64/arm64/armv7) in Docker - Add Makefile target for building agent binaries locally - Add startup validation to check for missing agent binaries - Improve download endpoint error messages with troubleshooting guidance - Enhance host details drawer layout with better organization and visual hierarchy - Update base images to rolling versions (node:20-alpine, golang:1.24-alpine, alpine:3.20)
This commit is contained in:
49
Dockerfile
49
Dockerfile
@@ -2,7 +2,7 @@
|
||||
ARG BUILD_AGENT=1
|
||||
|
||||
# Build stage for frontend (must be built first for embedding)
|
||||
FROM node:20.16.0-alpine3.19 AS frontend-builder
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/frontend-modern
|
||||
|
||||
@@ -19,7 +19,7 @@ RUN --mount=type=cache,id=pulse-npm-cache,target=/root/.npm \
|
||||
npm run build
|
||||
|
||||
# Build stage for Go backend
|
||||
FROM golang:1.24.9-alpine3.19 AS backend-builder
|
||||
FROM golang:1.24-alpine AS backend-builder
|
||||
|
||||
ARG BUILD_AGENT
|
||||
WORKDIR /app
|
||||
@@ -77,6 +77,34 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
||||
fi && \
|
||||
cp pulse-docker-agent-linux-amd64 pulse-docker-agent
|
||||
|
||||
# Build host-agent binaries for all platforms (for download endpoint)
|
||||
RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=pulse-go-build,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-linux-amd64 ./cmd/pulse-host-agent && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-linux-arm64 ./cmd/pulse-host-agent && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-linux-armv7 ./cmd/pulse-host-agent && \
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-darwin-amd64 ./cmd/pulse-host-agent && \
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-darwin-arm64 ./cmd/pulse-host-agent && \
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-trimpath \
|
||||
-o pulse-host-agent-windows-amd64.exe ./cmd/pulse-host-agent
|
||||
|
||||
# Build pulse-sensor-proxy
|
||||
RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=pulse-go-build,target=/root/.cache/go-build \
|
||||
@@ -86,7 +114,7 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
||||
-o pulse-sensor-proxy ./cmd/pulse-sensor-proxy
|
||||
|
||||
# Runtime image for the Docker agent (offered via --target agent_runtime)
|
||||
FROM alpine:3.19.1 AS agent_runtime
|
||||
FROM alpine:3.20 AS agent_runtime
|
||||
|
||||
# Use TARGETARCH to select the correct binary for the build platform
|
||||
ARG TARGETARCH
|
||||
@@ -118,7 +146,7 @@ ENV PULSE_NO_AUTO_UPDATE=true
|
||||
ENTRYPOINT ["/usr/local/bin/pulse-docker-agent"]
|
||||
|
||||
# Final stage (Pulse server runtime)
|
||||
FROM alpine:3.19.1
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata su-exec openssh-client
|
||||
|
||||
@@ -138,9 +166,10 @@ RUN chmod +x /docker-entrypoint.sh
|
||||
# Provide installer scripts for HTTP download endpoints
|
||||
RUN mkdir -p /opt/pulse/scripts
|
||||
COPY scripts/install-docker-agent.sh /opt/pulse/scripts/install-docker-agent.sh
|
||||
COPY scripts/install-host-agent.sh /opt/pulse/scripts/install-host-agent.sh
|
||||
COPY scripts/install-sensor-proxy.sh /opt/pulse/scripts/install-sensor-proxy.sh
|
||||
COPY scripts/install-docker.sh /opt/pulse/scripts/install-docker.sh
|
||||
RUN chmod 755 /opt/pulse/scripts/install-docker-agent.sh /opt/pulse/scripts/install-sensor-proxy.sh /opt/pulse/scripts/install-docker.sh
|
||||
RUN chmod 755 /opt/pulse/scripts/install-docker-agent.sh /opt/pulse/scripts/install-host-agent.sh /opt/pulse/scripts/install-sensor-proxy.sh /opt/pulse/scripts/install-docker.sh
|
||||
|
||||
# Copy multi-arch docker-agent binaries for download endpoint
|
||||
RUN mkdir -p /opt/pulse/bin
|
||||
@@ -149,6 +178,16 @@ COPY --from=backend-builder /app/pulse-docker-agent-linux-arm64 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-docker-agent-linux-armv7 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-docker-agent /opt/pulse/bin/pulse-docker-agent
|
||||
|
||||
# Copy multi-arch host-agent binaries for download endpoint
|
||||
COPY --from=backend-builder /app/pulse-host-agent-linux-amd64 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-host-agent-linux-arm64 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-host-agent-linux-armv7 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-host-agent-darwin-amd64 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-host-agent-darwin-arm64 /opt/pulse/bin/
|
||||
COPY --from=backend-builder /app/pulse-host-agent-windows-amd64.exe /opt/pulse/bin/
|
||||
# Create symlink for Windows without .exe extension
|
||||
RUN ln -s pulse-host-agent-windows-amd64.exe /opt/pulse/bin/pulse-host-agent-windows-amd64
|
||||
|
||||
# Copy pulse-sensor-proxy binary for download endpoint
|
||||
COPY --from=backend-builder /app/pulse-sensor-proxy /opt/pulse/bin/pulse-sensor-proxy
|
||||
|
||||
|
||||
19
Makefile
19
Makefile
@@ -1,13 +1,13 @@
|
||||
# Pulse Makefile for development
|
||||
|
||||
.PHONY: build run dev frontend backend all clean distclean dev-hot lint lint-backend lint-frontend format format-backend format-frontend
|
||||
.PHONY: build run dev frontend backend all clean distclean dev-hot lint lint-backend lint-frontend format format-backend format-frontend build-agents
|
||||
|
||||
FRONTEND_DIR := frontend-modern
|
||||
FRONTEND_DIST := $(FRONTEND_DIR)/dist
|
||||
FRONTEND_EMBED_DIR := internal/api/frontend-modern
|
||||
|
||||
# Build everything
|
||||
all: frontend backend
|
||||
# Build everything (including all agent binaries)
|
||||
all: frontend backend build-agents
|
||||
|
||||
# Build frontend only
|
||||
frontend:
|
||||
@@ -68,3 +68,16 @@ format-backend:
|
||||
|
||||
format-frontend:
|
||||
npm --prefix $(FRONTEND_DIR) run format
|
||||
|
||||
# Build all host agent binaries for all platforms
|
||||
build-agents:
|
||||
@echo "Building host agent binaries for all platforms..."
|
||||
@mkdir -p bin
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-linux-amd64 ./cmd/pulse-host-agent
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-linux-arm64 ./cmd/pulse-host-agent
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-linux-armv7 ./cmd/pulse-host-agent
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-darwin-amd64 ./cmd/pulse-host-agent
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-darwin-arm64 ./cmd/pulse-host-agent
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/pulse-host-agent-windows-amd64.exe ./cmd/pulse-host-agent
|
||||
@ln -sf pulse-host-agent-windows-amd64.exe bin/pulse-host-agent-windows-amd64
|
||||
@echo "✓ All host agent binaries built in bin/"
|
||||
|
||||
@@ -99,6 +99,9 @@ func runServer() {
|
||||
|
||||
log.Info().Msg("Starting Pulse monitoring server")
|
||||
|
||||
// Validate agent binaries are available for download
|
||||
validateAgentBinaries()
|
||||
|
||||
// Create context that cancels on interrupt
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -345,3 +348,46 @@ func performAutoImport() error {
|
||||
log.Info().Msg("Configuration auto-imported successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAgentBinaries checks if agent binaries are available for download
|
||||
// and logs warnings if any are missing
|
||||
func validateAgentBinaries() {
|
||||
binDirs := []string{"/opt/pulse/bin", "./bin", "."}
|
||||
platforms := []struct {
|
||||
name string
|
||||
file string
|
||||
}{
|
||||
{"linux-amd64", "pulse-host-agent-linux-amd64"},
|
||||
{"linux-arm64", "pulse-host-agent-linux-arm64"},
|
||||
{"linux-armv7", "pulse-host-agent-linux-armv7"},
|
||||
{"darwin-amd64", "pulse-host-agent-darwin-amd64"},
|
||||
{"darwin-arm64", "pulse-host-agent-darwin-arm64"},
|
||||
{"windows-amd64", "pulse-host-agent-windows-amd64"},
|
||||
}
|
||||
|
||||
missing := []string{}
|
||||
searchedPaths := []string{}
|
||||
|
||||
for _, platform := range platforms {
|
||||
found := false
|
||||
for _, dir := range binDirs {
|
||||
path := filepath.Join(dir, platform.file)
|
||||
searchedPaths = append(searchedPaths, path)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
missing = append(missing, platform.name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
log.Warn().
|
||||
Strs("missing_platforms", missing).
|
||||
Msg("Host agent binaries missing - install script downloads will fail. Rebuild Docker image or run build-release.sh to generate all platform binaries.")
|
||||
} else {
|
||||
log.Info().Msg("All host agent binaries available for download")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,41 +333,41 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||
|
||||
{/* Drawer - Additional Info */}
|
||||
<Show when={drawerOpen() && hasDrawerContent()}>
|
||||
<tr class="text-[11px] bg-gray-50/60 text-gray-600 dark:bg-gray-800/40 dark:text-gray-300">
|
||||
<td class="px-4 py-2" colSpan={5}>
|
||||
<div class="grid w-full gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<tr class="bg-gray-50 dark:bg-gray-900/50">
|
||||
<td class="px-4 py-3" colSpan={5}>
|
||||
<div class="flex flex-wrap justify-start gap-3">
|
||||
{/* System Info */}
|
||||
<div class="rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">System</div>
|
||||
<div class="mt-1 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">System</div>
|
||||
<div class="mt-2 space-y-1 text-[11px] text-gray-600 dark:text-gray-300">
|
||||
<Show when={host.cpuCount}>
|
||||
<div class="flex items-start gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 min-w-[60px]">CPUs:</span>
|
||||
<span>{host.cpuCount}</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">CPUs</span>
|
||||
<span class="text-right text-gray-600 dark:text-gray-300">{host.cpuCount}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={host.loadAverage && host.loadAverage.length > 0}>
|
||||
<div class="flex items-start gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 min-w-[60px]">Load Avg:</span>
|
||||
<span>{host.loadAverage!.map(l => l.toFixed(2)).join(', ')}</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">Load Avg</span>
|
||||
<span class="text-right text-gray-600 dark:text-gray-300">{host.loadAverage!.map(l => l.toFixed(2)).join(', ')}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={host.architecture}>
|
||||
<div class="flex items-start gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 min-w-[60px]">Arch:</span>
|
||||
<span>{host.architecture}</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">Arch</span>
|
||||
<span class="text-right text-gray-600 dark:text-gray-300">{host.architecture}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={host.kernelVersion}>
|
||||
<div class="flex items-start gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 min-w-[60px]">Kernel:</span>
|
||||
<span class="break-all">{host.kernelVersion}</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">Kernel</span>
|
||||
<span class="text-right text-gray-600 dark:text-gray-300 truncate">{host.kernelVersion}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={host.agentVersion}>
|
||||
<div class="flex items-start gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 min-w-[60px]">Agent:</span>
|
||||
<span>{host.agentVersion}</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">Agent</span>
|
||||
<span class="text-right text-gray-600 dark:text-gray-300">{host.agentVersion}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -375,15 +375,15 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||
|
||||
{/* Network Interfaces */}
|
||||
<Show when={host.networkInterfaces && host.networkInterfaces.length > 0}>
|
||||
<div class="rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Network</div>
|
||||
<div class="mt-1 space-y-1">
|
||||
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">Network</div>
|
||||
<div class="mt-2 space-y-2 text-[11px]">
|
||||
<For each={host.networkInterfaces?.slice(0, 4)}>
|
||||
{(iface) => (
|
||||
<div class="text-gray-600 dark:text-gray-300">
|
||||
<div class="font-medium">{iface.name}</div>
|
||||
<div class="rounded border border-dashed border-gray-200 p-2 dark:border-gray-700/70">
|
||||
<div class="font-medium text-gray-700 dark:text-gray-200">{iface.name}</div>
|
||||
<Show when={iface.addresses && iface.addresses.length > 0}>
|
||||
<div class="flex flex-wrap gap-1 mt-0.5">
|
||||
<div class="flex flex-wrap gap-1 mt-1 text-[10px]">
|
||||
<For each={iface.addresses}>
|
||||
{(addr) => (
|
||||
<span class="rounded bg-blue-100 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
|
||||
@@ -402,22 +402,22 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||
|
||||
{/* Disk Info */}
|
||||
<Show when={host.disks && host.disks.length > 0}>
|
||||
<div class="rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Disks</div>
|
||||
<div class="mt-1 space-y-1">
|
||||
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">Disks</div>
|
||||
<div class="mt-2 space-y-2 text-[11px]">
|
||||
<For each={host.disks?.slice(0, 3)}>
|
||||
{(disk) => {
|
||||
const diskPercent = () => disk.usage ?? 0;
|
||||
return (
|
||||
<div class="text-gray-600 dark:text-gray-300">
|
||||
<div class="rounded border border-dashed border-gray-200 p-2 dark:border-gray-700/70">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium truncate">{disk.mountpoint || disk.device}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200 truncate">{disk.mountpoint || disk.device}</span>
|
||||
<span class="text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{formatBytes(disk.used ?? 0, 0)} / {formatBytes(disk.total ?? 0, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={diskPercent() > 0}>
|
||||
<div class="mt-0.5">
|
||||
<div class="mt-1">
|
||||
<MetricBar
|
||||
value={diskPercent()}
|
||||
label={formatPercent(diskPercent())}
|
||||
@@ -435,14 +435,14 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||
|
||||
{/* Temperature Sensors */}
|
||||
<Show when={host.sensors?.temperatureCelsius && Object.keys(host.sensors.temperatureCelsius).length > 0}>
|
||||
<div class="rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Temperatures</div>
|
||||
<div class="mt-1 space-y-1 text-gray-600 dark:text-gray-300">
|
||||
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">Temperatures</div>
|
||||
<div class="mt-2 space-y-1 text-[11px] text-gray-600 dark:text-gray-300">
|
||||
<For each={Object.entries(host.sensors!.temperatureCelsius!).slice(0, 5)}>
|
||||
{([name, temp]) => (
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="truncate text-[10px]">{name}</span>
|
||||
<span class={temp > 80 ? 'text-red-600 dark:text-red-400 font-semibold' : ''}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200 truncate">{name}</span>
|
||||
<span class={`text-right ${temp > 80 ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-gray-600 dark:text-gray-300'}`}>
|
||||
{temp.toFixed(1)}°C
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3302,7 +3302,21 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Host agent binary not found. Please build from source: go build ./cmd/pulse-host-agent", http.StatusNotFound)
|
||||
// Build detailed error message with troubleshooting guidance
|
||||
var errorMsg strings.Builder
|
||||
errorMsg.WriteString(fmt.Sprintf("Host agent binary not found for %s/%s\n\n", platformParam, archParam))
|
||||
errorMsg.WriteString("Troubleshooting:\n")
|
||||
errorMsg.WriteString("1. If running in Docker: Rebuild the Docker image to include all platform binaries\n")
|
||||
errorMsg.WriteString("2. If running bare metal: Run 'scripts/build-release.sh' to build all platform binaries\n")
|
||||
errorMsg.WriteString("3. Build from source:\n")
|
||||
errorMsg.WriteString(fmt.Sprintf(" GOOS=%s GOARCH=%s go build -o pulse-host-agent-%s-%s ./cmd/pulse-host-agent\n", platformParam, archParam, platformParam, archParam))
|
||||
errorMsg.WriteString(fmt.Sprintf(" sudo mv pulse-host-agent-%s-%s /opt/pulse/bin/\n\n", platformParam, archParam))
|
||||
errorMsg.WriteString("Searched locations:\n")
|
||||
for _, path := range searchPaths {
|
||||
errorMsg.WriteString(fmt.Sprintf(" - %s\n", path))
|
||||
}
|
||||
|
||||
http.Error(w, errorMsg.String(), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// serveChecksum computes and serves the SHA256 checksum of a file
|
||||
|
||||
Reference in New Issue
Block a user