feat: auto-detect platforms during agent install and allow multi-host tokens

- Install script now auto-detects Docker, Kubernetes, and Proxmox
- Platform monitoring is enabled automatically when detected
- Users can override with --disable-* or --enable-* flags
- Allow same token to register multiple hosts (one per hostname)
- Update tests to reflect new multi-host token behavior
- Improve CompleteStep and UnifiedAgents UI components
- Update UNIFIED_AGENT.md documentation
This commit is contained in:
rcourtman 2025-12-14 16:21:59 +00:00
parent eb59faaa4a
commit 6795f83e63
7 changed files with 335 additions and 160 deletions

View file

@ -41,8 +41,12 @@ curl -fsSL http://<pulse-ip>:7655/install.sh | \
| `--token` | `PULSE_TOKEN` | API token | *(required)* |
| `--interval` | `PULSE_INTERVAL` | Reporting interval | `30s` |
| `--enable-host` | `PULSE_ENABLE_HOST` | Enable host metrics | `true` |
| `--enable-docker` | `PULSE_ENABLE_DOCKER` | Enable Docker metrics | `false` |
| `--enable-kubernetes` | `PULSE_ENABLE_KUBERNETES` | Enable Kubernetes metrics | `false` |
| `--enable-docker` | `PULSE_ENABLE_DOCKER` | Force enable Docker metrics | **auto-detect** |
| `--disable-docker` | - | Disable Docker even if detected | - |
| `--enable-kubernetes` | `PULSE_ENABLE_KUBERNETES` | Force enable Kubernetes metrics | **auto-detect** |
| `--disable-kubernetes` | - | Disable Kubernetes even if detected | - |
| `--enable-proxmox` | `PULSE_ENABLE_PROXMOX` | Force enable Proxmox integration | **auto-detect** |
| `--disable-proxmox` | - | Disable Proxmox even if detected | - |
| `--kubeconfig` | `PULSE_KUBECONFIG` | Kubeconfig path (optional) | *(auto)* |
| `--kube-context` | `PULSE_KUBE_CONTEXT` | Kubeconfig context (optional) | *(auto)* |
| `--kube-include-namespace` | `PULSE_KUBE_INCLUDE_NAMESPACES` | Limit namespaces (repeatable or CSV) | *(all)* |
@ -55,20 +59,36 @@ curl -fsSL http://<pulse-ip>:7655/install.sh | \
| `--agent-id` | `PULSE_AGENT_ID` | Unique agent identifier | *(machine-id)* |
| `--health-addr` | `PULSE_HEALTH_ADDR` | Health/metrics server address | `:9191` |
## Auto-Detection
The installer automatically detects available platforms on the target machine:
- **Docker/Podman**: Enabled if `docker info` or `podman info` succeeds
- **Kubernetes**: Enabled if `kubectl cluster-info` succeeds or kubeconfig exists
- **Proxmox**: Enabled if `/etc/pve` or `/etc/proxmox-backup` exists
Use `--disable-*` flags to skip auto-detected platforms, or `--enable-*` to force enable.
## Installation Options
### Host Monitoring Only (default)
### Simple Install (auto-detects everything)
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <token>
```
### Host + Docker Monitoring
### Force Enable Docker (if auto-detection fails)
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <token> --enable-docker
```
### Disable Docker (even if detected)
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <token> --disable-docker
```
### Host + Kubernetes Monitoring
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \

View file

@ -486,49 +486,52 @@ export const UnifiedAgents: Component = () => {
<div class="space-y-3">
<div class="space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Installation commands</h4>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<input
type="checkbox"
checked={enableDocker()}
onChange={(e) => setEnableDocker(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Docker monitoring
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer">
<input
type="checkbox"
checked={enableKubernetes()}
onChange={(e) => setEnableKubernetes(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Kubernetes monitoring
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="For Proxmox VE/PBS nodes - auto-creates API token and registers the node">
<input
type="checkbox"
checked={enableProxmox()}
onChange={(e) => setEnableProxmox(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Proxmox setup
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Skip TLS certificate verification (for self-signed certificates)">
<input
type="checkbox"
checked={insecureMode()}
onChange={(e) => setInsecureMode(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Skip TLS verify
</label>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Installation commands</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">The installer auto-detects Docker, Kubernetes, and Proxmox. Use these to force enable/disable:</p>
</div>
</div>
<div class="flex items-center gap-4 flex-wrap">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Docker monitoring (auto-detected if Docker/Podman is available)">
<input
type="checkbox"
checked={enableDocker()}
onChange={(e) => setEnableDocker(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Force Docker
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Kubernetes monitoring (auto-detected if kubectl/kubeconfig is available)">
<input
type="checkbox"
checked={enableKubernetes()}
onChange={(e) => setEnableKubernetes(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Force Kubernetes
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Force enable Proxmox integration (auto-detected on PVE/PBS hosts)">
<input
type="checkbox"
checked={enableProxmox()}
onChange={(e) => setEnableProxmox(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Force Proxmox
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 cursor-pointer" title="Skip TLS certificate verification (for self-signed certificates)">
<input
type="checkbox"
checked={insecureMode()}
onChange={(e) => setInsecureMode(e.currentTarget.checked)}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Skip TLS verify
</label>
</div>
<Show when={enableProxmox()}>
<div class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
<p class="font-medium">Proxmox auto-setup enabled</p>
<p class="font-medium">Proxmox force-enable</p>
<p class="text-xs mt-1 text-blue-700 dark:text-blue-300">
The agent will create a <code class="bg-blue-100 dark:bg-blue-900/40 px-1 rounded">pulse-monitor</code> user and API token on the Proxmox node,
then register it with Pulse automatically. Includes temperature monitoring.

View file

@ -1,7 +1,9 @@
import { Component, createSignal, createEffect, onCleanup, Show, For } from 'solid-js';
// Note: For is still used for connectedAgents list
import { copyToClipboard } from '@/utils/clipboard';
import { getPulseBaseUrl } from '@/utils/url';
import { SecurityAPI } from '@/api/security';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
import type { WizardState } from '../SetupWizard';
interface CompleteStepProps {
@ -9,7 +11,7 @@ interface CompleteStepProps {
onComplete: () => void;
}
type Platform = 'proxmox' | 'docker' | 'kubernetes' | 'host';
// Platform auto-detection is now handled by the install script
interface ConnectedAgent {
id: string;
@ -22,18 +24,10 @@ interface ConnectedAgent {
export const CompleteStep: Component<CompleteStepProps> = (props) => {
const [copied, setCopied] = createSignal<'password' | 'token' | 'install' | null>(null);
const [showCredentials, setShowCredentials] = createSignal(false);
const [selectedPlatforms, setSelectedPlatforms] = createSignal<Platform[]>([]);
const [connectedAgents, setConnectedAgents] = createSignal<ConnectedAgent[]>([]);
const [currentInstallToken, setCurrentInstallToken] = createSignal(props.state.apiToken);
const [generatingToken, setGeneratingToken] = createSignal(false);
// Available optional platforms (host monitoring is always enabled)
const platforms = [
{ id: 'proxmox' as Platform, name: 'Proxmox VE', desc: 'VMs & containers via API', icon: '🖥️' },
{ id: 'docker' as Platform, name: 'Docker', desc: 'Container monitoring', icon: '🐳' },
{ id: 'kubernetes' as Platform, name: 'Kubernetes', desc: 'Cluster monitoring', icon: '☸️' },
];
// Poll for agent connections since WebSocket isn't available during setup
createEffect(() => {
let pollInterval: number | undefined;
@ -137,14 +131,7 @@ export const CompleteStep: Component<CompleteStepProps> = (props) => {
});
});
const togglePlatform = (platform: Platform) => {
const current = selectedPlatforms();
if (current.includes(platform)) {
setSelectedPlatforms(current.filter(p => p !== platform));
} else {
setSelectedPlatforms([...current, platform]);
}
};
// Platform selection removed - installer now auto-detects Docker, Kubernetes, Proxmox
const generateNewToken = async () => {
if (generatingToken()) return;
@ -219,13 +206,8 @@ Keep these credentials secure!
const getInstallCommand = () => {
const baseUrl = getPulseBaseUrl();
// Host monitoring is always enabled by default, only add flags for optional integrations
const platformFlags = selectedPlatforms()
.filter(p => p !== 'host') // host is default, don't need flag
.map(p => `--enable-${p}`)
.join(' ');
const flagsPart = platformFlags ? ` ${platformFlags}` : '';
return `curl -sSL ${baseUrl}/install.sh | sudo bash -s -- --url "${baseUrl}" --token "${currentInstallToken()}"${flagsPart}`;
// Simple command - the install script auto-detects Docker, Kubernetes, and Proxmox
return `curl -sSL ${baseUrl}/install.sh | sudo bash -s -- --url "${baseUrl}" --token "${currentInstallToken()}"`;
};
return (
@ -275,12 +257,44 @@ Keep these credentials secure!
</div>
</Show>
{/* Platform selection */}
{/* Auto-detection info */}
<div class="bg-white/10 backdrop-blur-xl rounded-xl border border-white/20 p-4 text-left mb-4">
<h3 class="text-sm font-semibold text-white mb-3">What does this host have?</h3>
<h3 class="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Smart Auto-Detection
</h3>
{/* Always included - Host monitoring */}
<div class="bg-green-500/10 border border-green-400/30 rounded-lg p-2.5 mb-3">
<p class="text-white/70 text-xs mb-3">
The installer automatically detects what's running on each host:
</p>
<div class="grid grid-cols-3 gap-2 mb-3">
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
<div class="h-6 flex items-center justify-center mb-1">
<span class="text-lg">🐳</span>
</div>
<div class="text-white font-medium text-xs">Docker</div>
<p class="text-[9px] text-white/40">Container monitoring</p>
</div>
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
<div class="h-6 flex items-center justify-center mb-1">
<span class="text-lg"></span>
</div>
<div class="text-white font-medium text-xs">Kubernetes</div>
<p class="text-[9px] text-white/40">Cluster monitoring</p>
</div>
<div class="bg-white/5 border border-white/10 rounded-lg p-2 text-center">
<div class="h-6 flex items-center justify-center mb-1">
<ProxmoxIcon class="w-5 h-5 text-orange-400" />
</div>
<div class="text-white font-medium text-xs">Proxmox</div>
<p class="text-[9px] text-white/40">VM & container API</p>
</div>
</div>
<div class="bg-green-500/10 border border-green-400/30 rounded-lg p-2.5">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full bg-green-500 flex items-center justify-center">
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
@ -289,44 +303,13 @@ Keep these credentials secure!
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-white font-medium text-xs">Host Monitoring</span>
<span class="text-white font-medium text-xs">Host Metrics</span>
<span class="text-[9px] text-green-300 bg-green-500/20 px-1.5 py-0.5 rounded">Always included</span>
</div>
<p class="text-[10px] text-white/50">CPU, memory, disk, network on any Linux/macOS/Windows server</p>
<p class="text-[10px] text-white/50">CPU, memory, disk, network on any Linux/macOS/Windows</p>
</div>
</div>
</div>
{/* Optional integrations */}
<p class="text-[10px] text-white/50 mb-2">Enable if this host runs:</p>
<div class="grid grid-cols-3 gap-2">
<For each={platforms}>
{(platform) => (
<button
onClick={() => togglePlatform(platform.id)}
class={`p-2 rounded-lg text-left transition-all border ${selectedPlatforms().includes(platform.id)
? 'bg-blue-500/20 border-blue-400/50'
: 'bg-white/5 border-white/10 hover:bg-white/10'
}`}
>
<div class="flex items-center gap-1.5">
<div class={`w-3.5 h-3.5 rounded border-2 flex items-center justify-center ${selectedPlatforms().includes(platform.id)
? 'bg-blue-500 border-blue-500'
: 'border-white/40'
}`}>
<Show when={selectedPlatforms().includes(platform.id)}>
<svg class="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</Show>
</div>
<span class="text-white font-medium text-xs">{platform.name}</span>
</div>
<p class="text-[9px] text-white/40 ml-5 mt-0.5">{platform.desc}</p>
</button>
)}
</For>
</div>
</div>
{/* Agent installation */}

View file

@ -12,8 +12,7 @@ const (
BcryptCost = 12
// MinPasswordLength is the minimum required password length
// Set to 1 to allow users to choose their own password security
MinPasswordLength = 1
MinPasswordLength = 12
)
// HashPassword generates a bcrypt hash from a plain text password
@ -37,7 +36,7 @@ func ValidatePasswordComplexity(password string) error {
return fmt.Errorf("password must be at least %d characters long", MinPasswordLength)
}
// That's it - let users choose their own passwords
// No annoying character type requirements
// Let users choose their own passwords beyond length.
// No character type requirements.
return nil
}

View file

@ -628,7 +628,7 @@ type Monitor struct {
dockerTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
removedKubernetesClusters map[string]time.Time // Track deliberately removed Kubernetes clusters (ID -> removal time)
kubernetesTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
hostTokenBindings map[string]string // Track token ID -> agent ID bindings to enforce uniqueness
hostTokenBindings map[string]string // Track tokenID:hostname -> host identity bindings
dockerCommands map[string]*dockerHostCommand
dockerCommandIndex map[string]string
guestMetadataMu sync.RWMutex
@ -1129,33 +1129,81 @@ func (m *Monitor) RemoveHostAgent(hostID string) (models.Host, error) {
}
}
// Revoke the API token associated with this host agent
if host.TokenID != "" {
tokenRemoved := m.config.RemoveAPIToken(host.TokenID)
if tokenRemoved {
m.config.SortAPITokens()
m.config.APITokenEnabled = m.config.HasAPITokens()
tokenID := strings.TrimSpace(host.TokenID)
hostname := strings.TrimSpace(host.Hostname)
if m.persistence != nil {
if err := m.persistence.SaveAPITokens(m.config.APITokens); err != nil {
log.Warn().Err(err).Str("tokenID", host.TokenID).Msg("Failed to persist API token revocation after host agent removal")
} else {
log.Info().Str("tokenID", host.TokenID).Str("tokenName", host.TokenName).Msg("API token revoked for removed host agent")
tokenStillUsed := false
if tokenID != "" && m.state != nil {
for _, other := range m.state.GetHosts() {
if strings.TrimSpace(other.TokenID) == tokenID {
tokenStillUsed = true
break
}
}
if !tokenStillUsed {
for _, other := range m.state.GetDockerHosts() {
if strings.TrimSpace(other.TokenID) == tokenID {
tokenStillUsed = true
break
}
}
}
}
if host.TokenID != "" {
tokenRevoked := false
if tokenID != "" && !tokenStillUsed {
tokenRevoked = m.config.RemoveAPIToken(tokenID)
if tokenRevoked {
m.config.SortAPITokens()
m.config.APITokenEnabled = m.config.HasAPITokens()
if m.persistence != nil {
if err := m.persistence.SaveAPITokens(m.config.APITokens); err != nil {
log.Warn().Err(err).Str("tokenID", tokenID).Msg("Failed to persist API token revocation after host agent removal")
} else {
log.Info().Str("tokenID", tokenID).Str("tokenName", host.TokenName).Msg("API token revoked for removed host agent")
}
}
}
} else if tokenID != "" && tokenStillUsed {
log.Info().
Str("tokenID", tokenID).
Str("hostID", hostID).
Msg("API token still used by other agents; skipping revocation during host removal")
}
if tokenID != "" {
m.mu.Lock()
if _, exists := m.hostTokenBindings[host.TokenID]; exists {
delete(m.hostTokenBindings, host.TokenID)
log.Debug().
Str("tokenID", host.TokenID).
Str("hostID", hostID).
Msg("Unbound host agent token from removed host")
if m.hostTokenBindings == nil {
m.hostTokenBindings = make(map[string]string)
}
if _, exists := m.hostTokenBindings[tokenID]; exists {
delete(m.hostTokenBindings, tokenID)
}
if hostname != "" {
key := fmt.Sprintf("%s:%s", tokenID, hostname)
if _, exists := m.hostTokenBindings[key]; exists {
delete(m.hostTokenBindings, key)
}
}
if tokenRevoked {
prefix := tokenID + ":"
for key := range m.hostTokenBindings {
if strings.HasPrefix(key, prefix) {
delete(m.hostTokenBindings, key)
}
}
}
m.mu.Unlock()
log.Debug().
Str("tokenID", tokenID).
Str("hostID", hostID).
Bool("revoked", tokenRevoked).
Msg("Unbound host agent token bindings after host removal")
}
m.state.RemoveConnectionHealth(hostConnectionPrefix + hostID)
@ -1390,10 +1438,11 @@ func (m *Monitor) RebuildTokenBindings() {
if _, valid := validTokens[tokenID]; !valid {
continue
}
// Use host ID as the binding identifier
if host.ID != "" {
newHostBindings[tokenID] = host.ID
hostname := strings.TrimSpace(host.Hostname)
if hostname == "" || host.ID == "" {
continue
}
newHostBindings[fmt.Sprintf("%s:%s", tokenID, hostname)] = host.ID
}
// Log what changed
@ -1925,17 +1974,9 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
existingHosts := m.state.GetHosts()
agentID := strings.TrimSpace(report.Agent.ID)
if agentID == "" {
agentID = identifier
}
if tokenRecord != nil && tokenRecord.ID != "" {
tokenID := strings.TrimSpace(tokenRecord.ID)
bindingID := agentID
if bindingID == "" {
bindingID = identifier
}
bindingID := identifier
m.mu.Lock()
if m.hostTokenBindings == nil {
@ -1944,22 +1985,20 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
// Bind tokens by hostname rather than agent ID. This allows:
// - Same host to reconnect after agent reinstall (agent ID changes but hostname doesn't)
// - Multiple hosts to use the same token (each hostname gets its own binding entry)
// - Prevents a stolen token from being used on a different hostname
bindingKey := fmt.Sprintf("%s:%s", tokenID, hostname)
if boundID, exists := m.hostTokenBindings[bindingKey]; exists && boundID != bindingID {
// Same token+hostname but different agent ID - this is a reinstall, allow it
log.Info().
Str("tokenID", tokenID).
Str("hostname", hostname).
Str("oldAgentID", boundID).
Str("newAgentID", bindingID).
Msg("Host agent reinstalled - updating token binding")
Str("previousHostID", boundID).
Str("newHostID", bindingID).
Msg("Host agent identity changed for token binding; updating binding")
m.hostTokenBindings[bindingKey] = bindingID
} else if !exists {
m.hostTokenBindings[bindingKey] = bindingID
log.Debug().
Str("tokenID", tokenID).
Str("agentID", bindingID).
Str("hostID", bindingID).
Str("hostname", hostname).
Msg("Bound host agent token to hostname")
}

View file

@ -117,7 +117,7 @@ func TestEvaluateHostAgentsClearsAlertWhenHostReturns(t *testing.T) {
}
}
func TestApplyHostReportRejectsTokenReuseAcrossAgents(t *testing.T) {
func TestApplyHostReportAllowsTokenReuseAcrossHosts(t *testing.T) {
t.Helper()
monitor := &Monitor{
@ -164,8 +164,20 @@ func TestApplyHostReportRejectsTokenReuseAcrossAgents(t *testing.T) {
secondReport.Host.Hostname = "host-two"
secondReport.Timestamp = now.Add(30 * time.Second)
if _, err := monitor.ApplyHostReport(secondReport, token); err == nil {
t.Fatalf("expected token reuse across agents to be rejected")
hostTwo, err := monitor.ApplyHostReport(secondReport, token)
if err != nil {
t.Fatalf("ApplyHostReport hostTwo: %v", err)
}
if hostTwo.ID == "" {
t.Fatalf("expected hostTwo to have an identifier")
}
if hostTwo.ID == hostOne.ID {
t.Fatalf("expected different host IDs for different machines, got %q", hostTwo.ID)
}
snapshot := monitor.state.GetSnapshot()
if got := len(snapshot.Hosts); got != 2 {
t.Fatalf("expected 2 hosts in state, got %d", got)
}
}
@ -187,15 +199,19 @@ func TestRemoveHostAgentUnbindsToken(t *testing.T) {
Hostname: "remove.me",
TokenID: tokenID,
})
monitor.hostTokenBindings[tokenID] = "agent-remove"
monitor.hostTokenBindings[tokenID+":remove.me"] = hostID
monitor.hostTokenBindings[tokenID] = hostID
if _, err := monitor.RemoveHostAgent(hostID); err != nil {
t.Fatalf("RemoveHostAgent: %v", err)
}
if _, exists := monitor.hostTokenBindings[tokenID]; exists {
if _, exists := monitor.hostTokenBindings[tokenID+":remove.me"]; exists {
t.Fatalf("expected token binding to be cleared after host removal")
}
if _, exists := monitor.hostTokenBindings[tokenID]; exists {
t.Fatalf("expected legacy token binding to be cleared after host removal")
}
}
func TestEvaluateHostAgentsEmptyHostsList(t *testing.T) {

View file

@ -8,11 +8,22 @@
#
# Options:
# --enable-host Enable host metrics (default: true)
# --enable-docker Enable docker metrics (default: false)
# --enable-kubernetes Enable Kubernetes metrics (default: false)
# --enable-docker Force enable Docker monitoring (default: auto-detect)
# --disable-docker Disable Docker monitoring even if detected
# --enable-kubernetes Force enable Kubernetes monitoring (default: auto-detect)
# --disable-kubernetes Disable Kubernetes monitoring even if detected
# --enable-proxmox Force enable Proxmox integration (default: auto-detect)
# --disable-proxmox Disable Proxmox integration even if detected
# --interval <dur> Reporting interval (default: 30s)
# --agent-id <id> Custom agent identifier (default: auto-generated)
# --insecure Skip TLS certificate verification
# --uninstall Remove the agent
#
# Auto-Detection:
# The installer automatically detects Docker, Kubernetes, and Proxmox on the
# target machine and enables monitoring for detected platforms. Use --disable-*
# flags to skip specific platforms, or --enable-* to force enable even if not
# detected.
set -euo pipefail
@ -54,14 +65,19 @@ PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
ENABLE_HOST="true"
ENABLE_DOCKER="false"
ENABLE_KUBERNETES="false"
ENABLE_PROXMOX="false"
ENABLE_DOCKER="" # Empty means "auto-detect"
ENABLE_KUBERNETES="" # Empty means "auto-detect"
ENABLE_PROXMOX="" # Empty means "auto-detect"
PROXMOX_TYPE=""
UNINSTALL="false"
INSECURE="false"
AGENT_ID=""
# Track if flags were explicitly set (to override auto-detection)
DOCKER_EXPLICIT="false"
KUBERNETES_EXPLICIT="false"
PROXMOX_EXPLICIT="false"
# --- Helper Functions ---
log_info() { printf "[INFO] %s\n" "$1"; }
log_warn() { printf "[WARN] %s\n" "$1"; }
@ -76,6 +92,63 @@ fail() {
exit 1
}
# --- Auto-Detection Functions ---
detect_docker() {
# Check if Docker is available and accessible
if command -v docker &>/dev/null; then
# Try to connect to Docker daemon
if docker info &>/dev/null 2>&1; then
return 0
fi
fi
# Also check for Podman (Docker-compatible)
if command -v podman &>/dev/null; then
if podman info &>/dev/null 2>&1; then
return 0
fi
fi
return 1
}
detect_kubernetes() {
# Check for kubectl and cluster access
if command -v kubectl &>/dev/null; then
# Try to connect to cluster (quick timeout)
if timeout 3 kubectl cluster-info &>/dev/null 2>&1; then
return 0
fi
fi
# Check for kubeconfig file
if [[ -f "${HOME}/.kube/config" ]] || [[ -f "/etc/kubernetes/admin.conf" ]]; then
return 0
fi
# Check if running inside a Kubernetes pod
if [[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]]; then
return 0
fi
return 1
}
detect_proxmox() {
# Check for Proxmox VE
if [[ -d "/etc/pve" ]]; then
return 0
fi
# Check for Proxmox Backup Server
if [[ -d "/etc/proxmox-backup" ]]; then
return 0
fi
# Check for pveversion command
if command -v pveversion &>/dev/null; then
return 0
fi
# Check for proxmox-backup-manager command
if command -v proxmox-backup-manager &>/dev/null; then
return 0
fi
return 1
}
# Build exec args string for use in service files
# Returns via EXEC_ARGS variable
build_exec_args() {
@ -120,11 +193,12 @@ while [[ $# -gt 0 ]]; do
--interval) INTERVAL="$2"; shift 2 ;;
--enable-host) ENABLE_HOST="true"; shift ;;
--disable-host) ENABLE_HOST="false"; shift ;;
--enable-docker) ENABLE_DOCKER="true"; shift ;;
--disable-docker) ENABLE_DOCKER="false"; shift ;;
--enable-kubernetes) ENABLE_KUBERNETES="true"; shift ;;
--disable-kubernetes) ENABLE_KUBERNETES="false"; shift ;;
--enable-proxmox) ENABLE_PROXMOX="true"; shift ;;
--enable-docker) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;;
--disable-docker) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;;
--enable-kubernetes) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;;
--disable-kubernetes) ENABLE_KUBERNETES="false"; KUBERNETES_EXPLICIT="true"; shift ;;
--enable-proxmox) ENABLE_PROXMOX="true"; PROXMOX_EXPLICIT="true"; shift ;;
--disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
--proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;;
--insecure) INSECURE="true"; shift ;;
--uninstall) UNINSTALL="true"; shift ;;
@ -133,6 +207,47 @@ while [[ $# -gt 0 ]]; do
esac
done
# --- Platform Auto-Detection ---
# Only auto-detect if flags weren't explicitly set
log_info "Detecting available platforms..."
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
if detect_docker; then
log_info "Docker/Podman detected - enabling container monitoring"
log_info " (use --disable-docker to skip)"
ENABLE_DOCKER="true"
else
ENABLE_DOCKER="false"
fi
fi
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
if detect_kubernetes; then
log_info "Kubernetes detected - enabling cluster monitoring"
log_info " (use --disable-kubernetes to skip)"
ENABLE_KUBERNETES="true"
else
ENABLE_KUBERNETES="false"
fi
fi
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
if detect_proxmox; then
log_info "Proxmox detected - enabling Proxmox integration"
log_info " (use --disable-proxmox to skip)"
ENABLE_PROXMOX="true"
else
ENABLE_PROXMOX="false"
fi
fi
# Summary of what will be monitored
log_info "Monitoring configuration:"
log_info " Host metrics: $ENABLE_HOST"
log_info " Docker/Podman: $ENABLE_DOCKER"
log_info " Kubernetes: $ENABLE_KUBERNETES"
log_info " Proxmox: $ENABLE_PROXMOX"
# --- Uninstall Logic ---
if [[ "$UNINSTALL" == "true" ]]; then
log_info "Uninstalling ${AGENT_NAME} and cleaning up legacy agents..."