WIP: Save all pending changes including frontend updates and unified agent scaffolding
This commit is contained in:
parent
af2caa4b81
commit
26ebd476da
10 changed files with 987 additions and 573 deletions
8
Makefile
8
Makefile
|
|
@ -88,6 +88,12 @@ build-agents:
|
||||||
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=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=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=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
|
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 && \
|
||||||
|
echo "Building unified agent binaries..." && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.Version=v$$VERSION" -trimpath -o bin/pulse-agent-linux-amd64 ./cmd/pulse-agent && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.Version=v$$VERSION" -trimpath -o bin/pulse-agent-linux-arm64 ./cmd/pulse-agent && \
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.Version=v$$VERSION" -trimpath -o bin/pulse-agent-macos-amd64 ./cmd/pulse-agent && \
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.Version=v$$VERSION" -trimpath -o bin/pulse-agent-macos-arm64 ./cmd/pulse-agent && \
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.Version=v$$VERSION" -trimpath -o bin/pulse-agent-windows-amd64.exe ./cmd/pulse-agent
|
||||||
@ln -sf pulse-host-agent-windows-amd64.exe bin/pulse-host-agent-windows-amd64
|
@ln -sf pulse-host-agent-windows-amd64.exe bin/pulse-host-agent-windows-amd64
|
||||||
@echo "✓ All agent binaries built in bin/"
|
@echo "✓ All agent binaries built in bin/"
|
||||||
|
|
|
||||||
257
cmd/pulse-agent/main.go
Normal file
257
cmd/pulse-agent/main.go
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/dockeragent"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multiValue []string
|
||||||
|
|
||||||
|
func (m *multiValue) String() string {
|
||||||
|
return strings.Join(*m, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multiValue) Set(value string) error {
|
||||||
|
*m = append(*m, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. Parse Configuration
|
||||||
|
cfg := loadConfig()
|
||||||
|
|
||||||
|
// 2. Setup Logging
|
||||||
|
zerolog.SetGlobalLevel(cfg.LogLevel)
|
||||||
|
logger := zerolog.New(os.Stdout).Level(cfg.LogLevel).With().Timestamp().Logger()
|
||||||
|
cfg.Logger = &logger
|
||||||
|
|
||||||
|
// 3. Setup Context & Signal Handling
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
logger.Info().
|
||||||
|
Str("version", Version).
|
||||||
|
Str("pulse_url", cfg.PulseURL).
|
||||||
|
Bool("host_agent", cfg.EnableHost).
|
||||||
|
Bool("docker_agent", cfg.EnableDocker).
|
||||||
|
Msg("Starting Pulse Unified Agent")
|
||||||
|
|
||||||
|
// 4. Start Host Agent (if enabled)
|
||||||
|
if cfg.EnableHost {
|
||||||
|
hostCfg := hostagent.Config{
|
||||||
|
PulseURL: cfg.PulseURL,
|
||||||
|
APIToken: cfg.APIToken,
|
||||||
|
Interval: cfg.Interval,
|
||||||
|
HostnameOverride: cfg.HostnameOverride,
|
||||||
|
AgentID: cfg.AgentID, // Shared ID? Or separate? Usually separate for now.
|
||||||
|
Tags: cfg.Tags,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
LogLevel: cfg.LogLevel,
|
||||||
|
Logger: &logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If AgentID is set globally, we might want to suffix it or let the agents derive their own.
|
||||||
|
// For now, let's pass it through. If it's empty, agents derive their own.
|
||||||
|
|
||||||
|
agent, err := hostagent.New(hostCfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("Failed to initialize host agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
logger.Info().Msg("Host agent module started")
|
||||||
|
return agent.Run(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Start Docker Agent (if enabled)
|
||||||
|
if cfg.EnableDocker {
|
||||||
|
dockerCfg := dockeragent.Config{
|
||||||
|
PulseURL: cfg.PulseURL,
|
||||||
|
APIToken: cfg.APIToken,
|
||||||
|
Interval: cfg.Interval,
|
||||||
|
HostnameOverride: cfg.HostnameOverride,
|
||||||
|
AgentID: cfg.AgentID,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||||
|
DisableAutoUpdate: true, // Unified agent handles updates (future)
|
||||||
|
LogLevel: cfg.LogLevel,
|
||||||
|
Logger: &logger,
|
||||||
|
// Docker specific defaults
|
||||||
|
SwarmScope: "node",
|
||||||
|
IncludeContainers: true,
|
||||||
|
IncludeServices: true,
|
||||||
|
IncludeTasks: true,
|
||||||
|
CollectDiskMetrics: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := dockeragent.New(dockerCfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("Failed to initialize docker agent")
|
||||||
|
}
|
||||||
|
// Docker agent has a Close method we should call, but errgroup doesn't make defer easy here.
|
||||||
|
// Ideally we wrap this. For now, we rely on OS cleanup or context cancellation.
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
logger.Info().Msg("Docker agent module started")
|
||||||
|
return agent.Run(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Wait for all agents to exit
|
||||||
|
if err := g.Wait(); err != nil && err != context.Canceled {
|
||||||
|
logger.Error().Err(err).Msg("Agent terminated with error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info().Msg("Pulse Unified Agent stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PulseURL string
|
||||||
|
APIToken string
|
||||||
|
Interval time.Duration
|
||||||
|
HostnameOverride string
|
||||||
|
AgentID string
|
||||||
|
Tags []string
|
||||||
|
InsecureSkipVerify bool
|
||||||
|
LogLevel zerolog.Level
|
||||||
|
Logger *zerolog.Logger
|
||||||
|
|
||||||
|
// Module flags
|
||||||
|
EnableHost bool
|
||||||
|
EnableDocker bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() Config {
|
||||||
|
// Environment Variables
|
||||||
|
envURL := utils.GetenvTrim("PULSE_URL")
|
||||||
|
envToken := utils.GetenvTrim("PULSE_TOKEN")
|
||||||
|
envInterval := utils.GetenvTrim("PULSE_INTERVAL")
|
||||||
|
envHostname := utils.GetenvTrim("PULSE_HOSTNAME")
|
||||||
|
envAgentID := utils.GetenvTrim("PULSE_AGENT_ID")
|
||||||
|
envInsecure := utils.GetenvTrim("PULSE_INSECURE_SKIP_VERIFY")
|
||||||
|
envTags := utils.GetenvTrim("PULSE_TAGS")
|
||||||
|
envLogLevel := utils.GetenvTrim("LOG_LEVEL")
|
||||||
|
envEnableHost := utils.GetenvTrim("PULSE_ENABLE_HOST")
|
||||||
|
envEnableDocker := utils.GetenvTrim("PULSE_ENABLE_DOCKER")
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
defaultInterval := 30 * time.Second
|
||||||
|
if envInterval != "" {
|
||||||
|
if parsed, err := time.ParseDuration(envInterval); err == nil {
|
||||||
|
defaultInterval = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultEnableHost := true
|
||||||
|
if envEnableHost != "" {
|
||||||
|
defaultEnableHost = utils.ParseBool(envEnableHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultEnableDocker := false
|
||||||
|
if envEnableDocker != "" {
|
||||||
|
defaultEnableDocker = utils.ParseBool(envEnableDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
urlFlag := flag.String("url", envURL, "Pulse server URL")
|
||||||
|
tokenFlag := flag.String("token", envToken, "Pulse API token")
|
||||||
|
intervalFlag := flag.Duration("interval", defaultInterval, "Reporting interval")
|
||||||
|
hostnameFlag := flag.String("hostname", envHostname, "Override hostname")
|
||||||
|
agentIDFlag := flag.String("agent-id", envAgentID, "Override agent identifier")
|
||||||
|
insecureFlag := flag.Bool("insecure", utils.ParseBool(envInsecure), "Skip TLS verification")
|
||||||
|
logLevelFlag := flag.String("log-level", defaultLogLevel(envLogLevel), "Log level")
|
||||||
|
|
||||||
|
enableHostFlag := flag.Bool("enable-host", defaultEnableHost, "Enable Host Agent module")
|
||||||
|
enableDockerFlag := flag.Bool("enable-docker", defaultEnableDocker, "Enable Docker Agent module")
|
||||||
|
|
||||||
|
var tagFlags multiValue
|
||||||
|
flag.Var(&tagFlags, "tag", "Tag to apply (repeatable)")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
pulseURL := strings.TrimSpace(*urlFlag)
|
||||||
|
if pulseURL == "" {
|
||||||
|
pulseURL = "http://localhost:7655"
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(*tokenFlag)
|
||||||
|
if token == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "error: Pulse API token is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel, err := parseLogLevel(*logLevelFlag)
|
||||||
|
if err != nil {
|
||||||
|
logLevel = zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := gatherTags(envTags, tagFlags)
|
||||||
|
|
||||||
|
return Config{
|
||||||
|
PulseURL: pulseURL,
|
||||||
|
APIToken: token,
|
||||||
|
Interval: *intervalFlag,
|
||||||
|
HostnameOverride: strings.TrimSpace(*hostnameFlag),
|
||||||
|
AgentID: strings.TrimSpace(*agentIDFlag),
|
||||||
|
Tags: tags,
|
||||||
|
InsecureSkipVerify: *insecureFlag,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
EnableHost: *enableHostFlag,
|
||||||
|
EnableDocker: *enableDockerFlag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers (duplicated from existing agents for now, to be moved to shared pkg later)
|
||||||
|
func gatherTags(env string, flags []string) []string {
|
||||||
|
tags := make([]string, 0)
|
||||||
|
if env != "" {
|
||||||
|
for _, tag := range strings.Split(env, ",") {
|
||||||
|
tag = strings.TrimSpace(tag)
|
||||||
|
if tag != "" {
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tag := range flags {
|
||||||
|
tag = strings.TrimSpace(tag)
|
||||||
|
if tag != "" {
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLogLevel(value string) (zerolog.Level, error) {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if normalized == "" {
|
||||||
|
return zerolog.InfoLevel, nil
|
||||||
|
}
|
||||||
|
return zerolog.ParseLevel(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultLogLevel(envValue string) string {
|
||||||
|
if strings.TrimSpace(envValue) == "" {
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
return envValue
|
||||||
|
}
|
||||||
|
|
@ -346,24 +346,24 @@ const getRemovalStatusInfo = (host: DockerHost): { label: string; tone: RemovalS
|
||||||
const url = pulseUrl();
|
const url = pulseUrl();
|
||||||
const tokenValue = requiresToken() ? TOKEN_PLACEHOLDER : 'disabled';
|
const tokenValue = requiresToken() ? TOKEN_PLACEHOLDER : 'disabled';
|
||||||
const tokenSegment = `--token '${tokenValue}'`;
|
const tokenSegment = `--token '${tokenValue}'`;
|
||||||
return `curl -fSL '${url}/install-docker-agent.sh' -o /tmp/pulse-install-docker-agent.sh && sudo bash /tmp/pulse-install-docker-agent.sh --url '${url}' ${tokenSegment} && rm -f /tmp/pulse-install-docker-agent.sh`;
|
return `curl -fsSL '${url}/install.sh' | bash -s -- --url '${url}' ${tokenSegment} --enable-docker`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUninstallCommand = () => {
|
const getUninstallCommand = () => {
|
||||||
const url = pulseUrl();
|
const url = pulseUrl();
|
||||||
return `curl -fSL '${url}/install-docker-agent.sh' -o /tmp/pulse-install-docker-agent.sh && sudo bash /tmp/pulse-install-docker-agent.sh --uninstall && rm -f /tmp/pulse-install-docker-agent.sh`;
|
return `curl -fsSL '${url}/install.sh' | bash -s -- --uninstall`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSystemdService = () => {
|
const getSystemdService = () => {
|
||||||
const token = requiresToken() ? TOKEN_PLACEHOLDER : 'disabled';
|
const token = requiresToken() ? TOKEN_PLACEHOLDER : 'disabled';
|
||||||
return `[Unit]
|
return `[Unit]
|
||||||
Description=Pulse Docker Agent
|
Description=Pulse Unified Agent
|
||||||
After=network-online.target docker.service
|
After=network-online.target docker.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/local/bin/pulse-docker-agent --url ${pulseUrl()} --token ${token} --interval 30s
|
ExecStart=/usr/local/bin/pulse-agent --url ${pulseUrl()} --token ${token} --interval 30s --enable-docker
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
User=root
|
User=root
|
||||||
|
|
@ -686,7 +686,7 @@ WantedBy=multi-user.target`;
|
||||||
<code>{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}</code>
|
<code>{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
The installer downloads the agent, detects your container runtime, configures a systemd service, and starts reporting automatically.
|
The unified installer downloads the agent, detects your container runtime, configures a systemd service, and starts reporting automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -735,7 +735,7 @@ WantedBy=multi-user.target`;
|
||||||
<code>
|
<code>
|
||||||
cd /opt/pulse
|
cd /opt/pulse
|
||||||
<br />
|
<br />
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-docker-agent ./cmd/pulse-docker-agent
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-agent ./cmd/pulse-agent
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -744,9 +744,9 @@ WantedBy=multi-user.target`;
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
|
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
|
||||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||||
<code>
|
<code>
|
||||||
scp pulse-docker-agent user@docker-host:/usr/local/bin/
|
scp pulse-agent user@docker-host:/usr/local/bin/
|
||||||
<br />
|
<br />
|
||||||
ssh user@docker-host chmod +x /usr/local/bin/pulse-docker-agent
|
ssh user@docker-host chmod +x /usr/local/bin/pulse-agent
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd template</p>
|
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd template</p>
|
||||||
|
|
@ -772,7 +772,7 @@ WantedBy=multi-user.target`;
|
||||||
<code>
|
<code>
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
<br />
|
<br />
|
||||||
systemctl enable --now pulse-docker-agent
|
systemctl enable --now pulse-agent
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -832,8 +832,7 @@ WantedBy=multi-user.target`;
|
||||||
modalCommandStatus() === 'completed' ||
|
modalCommandStatus() === 'completed' ||
|
||||||
(modalHostPendingUninstall() && !modalHasCommand())
|
(modalHostPendingUninstall() && !modalHasCommand())
|
||||||
}
|
}
|
||||||
class={`inline-flex items-center justify-center rounded px-4 py-2 text-sm font-medium text-white transition-colors ${
|
class={`inline-flex items-center justify-center rounded px-4 py-2 text-sm font-medium text-white transition-colors ${modalCommandStatus() === 'completed'
|
||||||
modalCommandStatus() === 'completed'
|
|
||||||
? 'bg-emerald-600 dark:bg-emerald-500'
|
? 'bg-emerald-600 dark:bg-emerald-500'
|
||||||
: 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'
|
: 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'
|
||||||
} disabled:cursor-not-allowed disabled:opacity-60`}
|
} disabled:cursor-not-allowed disabled:opacity-60`}
|
||||||
|
|
@ -895,8 +894,7 @@ WantedBy=multi-user.target`;
|
||||||
class={`${step.done || step.active ? 'text-blue-700 dark:text-blue-200' : 'text-gray-500 dark:text-gray-400'} flex items-center gap-2 text-xs`}
|
class={`${step.done || step.active ? 'text-blue-700 dark:text-blue-200' : 'text-gray-500 dark:text-gray-400'} flex items-center gap-2 text-xs`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class={`relative h-2 w-2 flex-shrink-0 rounded-full ${
|
class={`relative h-2 w-2 flex-shrink-0 rounded-full ${step.done
|
||||||
step.done
|
|
||||||
? 'bg-blue-500'
|
? 'bg-blue-500'
|
||||||
: step.active
|
: step.active
|
||||||
? 'bg-blue-400 animate-pulse'
|
? 'bg-blue-400 animate-pulse'
|
||||||
|
|
@ -966,12 +964,12 @@ WantedBy=multi-user.target`;
|
||||||
Agent confirmed the stop. Pulse has already cleaned up everything it controls:
|
Agent confirmed the stop. Pulse has already cleaned up everything it controls:
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-2 space-y-1 leading-snug">
|
<ul class="mt-2 space-y-1 leading-snug">
|
||||||
<li>• Terminated the running <code class="font-mono text-[11px]">pulse-docker-agent</code> process</li>
|
<li>• Terminated the running <code class="font-mono text-[11px]">pulse-agent</code> process</li>
|
||||||
<li>• Disabled future auto-start (stops the systemd unit or removes the Unraid autostart script if one exists)</li>
|
<li>• Disabled future auto-start (stops the systemd unit or removes the Unraid autostart script if one exists)</li>
|
||||||
<li>• Cleared the host from the dashboard so new reports won’t appear unexpectedly</li>
|
<li>• Cleared the host from the dashboard so new reports won’t appear unexpectedly</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
The binary remains at <code class="font-mono text-[11px]">/usr/local/bin/pulse-docker-agent</code> for quick reinstalls. Use the uninstall command below if you prefer to remove it too.
|
The binary remains at <code class="font-mono text-[11px]">/usr/local/bin/pulse-agent</code> for quick reinstalls. Use the uninstall command below if you prefer to remove it too.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -996,8 +994,7 @@ WantedBy=multi-user.target`;
|
||||||
class={`${step.done || step.active ? 'text-blue-700 dark:text-blue-200' : 'text-gray-500 dark:text-gray-400'} flex items-center gap-2`}
|
class={`${step.done || step.active ? 'text-blue-700 dark:text-blue-200' : 'text-gray-500 dark:text-gray-400'} flex items-center gap-2`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class={`relative h-2 w-2 rounded-full ${
|
class={`relative h-2 w-2 rounded-full ${step.done
|
||||||
step.done
|
|
||||||
? 'bg-blue-500'
|
? 'bg-blue-500'
|
||||||
: step.active
|
: step.active
|
||||||
? 'bg-blue-400 animate-pulse'
|
? 'bg-blue-400 animate-pulse'
|
||||||
|
|
@ -1011,7 +1008,7 @@ WantedBy=multi-user.target`;
|
||||||
</ul>
|
</ul>
|
||||||
<p class="leading-snug">
|
<p class="leading-snug">
|
||||||
Pulse responds to the agent's <code class="font-mono text-[11px]">/api/agents/docker/report</code> call with a stop command. The agent disables its service, removes
|
Pulse responds to the agent's <code class="font-mono text-[11px]">/api/agents/docker/report</code> call with a stop command. The agent disables its service, removes
|
||||||
<code class="font-mono text-[11px]">/boot/config/go.d/pulse-docker-agent.sh</code>, and posts back to
|
<code class="font-mono text-[11px]">/boot/config/go.d/pulse-agent.sh</code>, and posts back to
|
||||||
<code class="font-mono text-[11px]">/api/agents/docker/commands/<id>/ack</code> so Pulse knows it can remove the row.
|
<code class="font-mono text-[11px]">/api/agents/docker/commands/<id>/ack</code> so Pulse knows it can remove the row.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1113,7 +1110,7 @@ WantedBy=multi-user.target`;
|
||||||
<p class="text-[11px] font-medium text-gray-600 dark:text-gray-300">Command copied to clipboard.</p>
|
<p class="text-[11px] font-medium text-gray-600 dark:text-gray-300">Command copied to clipboard.</p>
|
||||||
</Show>
|
</Show>
|
||||||
<p class="text-[11px] text-gray-500 dark:text-gray-400">
|
<p class="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
This command stops the agent, removes the systemd service (or Unraid autostart hook), deletes <code class="font-mono text-[11px]">/var/log/pulse-docker-agent.log</code>, and uninstalls the binary. Pulse will notice the host is gone after the next heartbeat (≈2 minutes) and clean up the row automatically.
|
This command stops the agent, removes the systemd service (or Unraid autostart hook), deletes <code class="font-mono text-[11px]">/var/log/pulse-agent.log</code>, and uninstalls the binary. Pulse will notice the host is gone after the next heartbeat (≈2 minutes) and clean up the row automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 rounded border border-gray-200 p-3 dark:border-gray-700">
|
<div class="flex flex-col gap-2 rounded border border-gray-200 p-3 dark:border-gray-700">
|
||||||
|
|
@ -1443,8 +1440,7 @@ WantedBy=multi-user.target`;
|
||||||
<td class="py-3 px-4 align-top">
|
<td class="py-3 px-4 align-top">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
class={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${isOnline
|
||||||
isOnline
|
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,14 @@ const commandsByPlatform: Record<
|
||||||
linux: {
|
linux: {
|
||||||
title: 'Install on Linux',
|
title: 'Install on Linux',
|
||||||
description:
|
description:
|
||||||
'The installer downloads the agent binary and configures it as a systemd service.',
|
'The unified installer downloads the agent binary and configures it as a systemd service.',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
label: 'Install with systemd',
|
label: 'Install with systemd',
|
||||||
command: `curl -fsSL ${pulseUrl()}/install-host-agent.sh | bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
command: `curl -fsSL ${pulseUrl()}/install.sh | bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
||||||
note: (
|
note: (
|
||||||
<span>
|
<span>
|
||||||
Automatically installs to <code>/usr/local/bin/pulse-host-agent</code> and creates <code>/etc/systemd/system/pulse-host-agent.service</code>.
|
Automatically installs to <code>/usr/local/bin/pulse-agent</code> and creates <code>/etc/systemd/system/pulse-agent.service</code>.
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -53,14 +53,14 @@ const commandsByPlatform: Record<
|
||||||
macos: {
|
macos: {
|
||||||
title: 'Install on macOS',
|
title: 'Install on macOS',
|
||||||
description:
|
description:
|
||||||
'The installer downloads the universal binary and sets up a launchd service for background monitoring.',
|
'The unified installer downloads the universal binary and sets up a launchd service for background monitoring.',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
label: 'Install with launchd',
|
label: 'Install with launchd',
|
||||||
command: `curl -fsSL ${pulseUrl()}/install-host-agent.sh | bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
command: `curl -fsSL ${pulseUrl()}/install.sh | bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
||||||
note: (
|
note: (
|
||||||
<span>
|
<span>
|
||||||
Creates <code>~/Library/LaunchAgents/com.pulse.host-agent.plist</code> and starts the agent automatically.
|
Creates <code>/Library/LaunchDaemons/com.pulse.agent.plist</code> and starts the agent automatically.
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -69,20 +69,20 @@ const commandsByPlatform: Record<
|
||||||
windows: {
|
windows: {
|
||||||
title: 'Install on Windows',
|
title: 'Install on Windows',
|
||||||
description:
|
description:
|
||||||
'Run the PowerShell script to install and configure the host agent as a Windows service with automatic startup.',
|
'Run the PowerShell script to install and configure the unified agent as a Windows service with automatic startup.',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
label: 'Install as Windows Service (PowerShell)',
|
label: 'Install as Windows Service (PowerShell)',
|
||||||
command: `irm ${pulseUrl()}/install-host-agent.ps1 | iex`,
|
command: `irm ${pulseUrl()}/install.ps1 | iex`,
|
||||||
note: (
|
note: (
|
||||||
<span>
|
<span>
|
||||||
Run in PowerShell as Administrator. The script will prompt for the Pulse URL and API token, download the agent binary, and install it as a Windows service with automatic startup. The agent runs natively and can access all Windows performance counters.
|
Run in PowerShell as Administrator. The script will prompt for the Pulse URL and API token, download the agent binary, and install it as a Windows service with automatic startup.
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Install with parameters (PowerShell)',
|
label: 'Install with parameters (PowerShell)',
|
||||||
command: `$env:PULSE_URL="${pulseUrl()}"; $env:PULSE_TOKEN="${TOKEN_PLACEHOLDER}"; irm ${pulseUrl()}/install-host-agent.ps1 | iex`,
|
command: `$env:PULSE_URL="${pulseUrl()}"; $env:PULSE_TOKEN="${TOKEN_PLACEHOLDER}"; irm ${pulseUrl()}/install.ps1 | iex`,
|
||||||
note: (
|
note: (
|
||||||
<span>
|
<span>
|
||||||
Non-interactive installation. Set environment variables before running to skip prompts.
|
Non-interactive installation. Set environment variables before running to skip prompts.
|
||||||
|
|
@ -538,26 +538,26 @@ User=root
|
||||||
WantedBy=multi-user.target`;
|
WantedBy=multi-user.target`;
|
||||||
|
|
||||||
function getManualUninstallCommand(): string {
|
function getManualUninstallCommand(): string {
|
||||||
return `sudo systemctl stop pulse-host-agent && \\
|
return `sudo systemctl stop pulse-agent && \\
|
||||||
sudo systemctl disable pulse-host-agent && \\
|
sudo systemctl disable pulse-agent && \\
|
||||||
sudo rm -f /etc/systemd/system/pulse-host-agent.service && \\
|
sudo rm -f /etc/systemd/system/pulse-agent.service && \\
|
||||||
sudo rm -f /usr/local/bin/pulse-host-agent && \\
|
sudo rm -f /usr/local/bin/pulse-agent && \\
|
||||||
sudo systemctl daemon-reload`;
|
sudo systemctl daemon-reload`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostUninstallCommand(host: Host | null): string {
|
function getHostUninstallCommand(host: Host | null): string {
|
||||||
const platform = host?.platform?.toLowerCase();
|
const platform = host?.platform?.toLowerCase();
|
||||||
if (platform === 'macos' || platform === 'darwin' || platform === 'mac') {
|
if (platform === 'macos' || platform === 'darwin' || platform === 'mac') {
|
||||||
return `launchctl unload ~/Library/LaunchAgents/com.pulse.host-agent.plist >/dev/null 2>&1 || true && \\
|
return `launchctl unload /Library/LaunchDaemons/com.pulse.agent.plist >/dev/null 2>&1 || true && \\
|
||||||
rm -f ~/Library/LaunchAgents/com.pulse.host-agent.plist && \\
|
rm -f /Library/LaunchDaemons/com.pulse.agent.plist && \\
|
||||||
sudo rm -f /usr/local/bin/pulse-host-agent && \\
|
sudo rm -f /usr/local/bin/pulse-agent && \\
|
||||||
rm -f ~/Library/Logs/pulse-host-agent.log`;
|
rm -f /var/log/pulse-agent.log`;
|
||||||
}
|
}
|
||||||
if (platform === 'windows' || platform === 'win32' || platform === 'windows_nt') {
|
if (platform === 'windows' || platform === 'win32' || platform === 'windows_nt') {
|
||||||
return `Stop-Service -Name PulseHostAgent -ErrorAction SilentlyContinue; \\
|
return `Stop-Service -Name PulseAgent -ErrorAction SilentlyContinue; \\
|
||||||
sc.exe delete PulseHostAgent; \\
|
sc.exe delete PulseAgent; \\
|
||||||
Remove-Item 'C:\\\\Program Files\\\\Pulse\\\\pulse-host-agent.exe' -Force -ErrorAction SilentlyContinue; \\
|
Remove-Item 'C:\\\\Program Files\\\\Pulse\\\\pulse-agent.exe' -Force -ErrorAction SilentlyContinue; \\
|
||||||
Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-host-agent.log' -Force -ErrorAction SilentlyContinue`;
|
Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-agent.log' -Force -ErrorAction SilentlyContinue`;
|
||||||
}
|
}
|
||||||
return getManualUninstallCommand();
|
return getManualUninstallCommand();
|
||||||
}
|
}
|
||||||
|
|
@ -628,8 +628,7 @@ Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-host-agent.log' -Force -ErrorAct
|
||||||
type="button"
|
type="button"
|
||||||
onClick={acknowledgeNoToken}
|
onClick={acknowledgeNoToken}
|
||||||
disabled={confirmedNoToken()}
|
disabled={confirmedNoToken()}
|
||||||
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${confirmedNoToken()
|
||||||
confirmedNoToken()
|
|
||||||
? 'bg-green-600 text-white cursor-default'
|
? 'bg-green-600 text-white cursor-default'
|
||||||
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -780,15 +779,15 @@ Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-host-agent.log' -Force -ErrorAct
|
||||||
<code>
|
<code>
|
||||||
cd /opt/pulse
|
cd /opt/pulse
|
||||||
<br />
|
<br />
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-host-agent ./cmd/pulse-host-agent
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-agent ./cmd/pulse-agent
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
|
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
|
||||||
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
|
||||||
<code>
|
<code>
|
||||||
scp pulse-host-agent user@host:/usr/local/bin/
|
scp pulse-agent user@host:/usr/local/bin/
|
||||||
<br />
|
<br />
|
||||||
ssh user@host sudo chmod +x /usr/local/bin/pulse-host-agent
|
ssh user@host sudo chmod +x /usr/local/bin/pulse-agent
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd service template</p>
|
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd service template</p>
|
||||||
|
|
@ -1010,8 +1009,7 @@ Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-host-agent.log' -Force -ErrorAct
|
||||||
<td class="py-3 px-4">
|
<td class="py-3 px-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
class={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${isOnline
|
||||||
isOnline
|
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -1191,8 +1189,7 @@ Remove-Item '$env:ProgramData\\\\Pulse\\\\pulse-host-agent.log' -Force -ErrorAct
|
||||||
<div class="flex items-center justify-between text-xs text-gray-600 dark:text-gray-300">
|
<div class="flex items-center justify-between text-xs text-gray-600 dark:text-gray-300">
|
||||||
<span class="font-semibold uppercase tracking-wide text-[11px] text-gray-500 dark:text-gray-400">Host status</span>
|
<span class="font-semibold uppercase tracking-wide text-[11px] text-gray-500 dark:text-gray-400">Host status</span>
|
||||||
<span
|
<span
|
||||||
class={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
class={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase ${hostRemovalIsOnline()
|
||||||
hostRemovalIsOnline()
|
|
||||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200'
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200'
|
||||||
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200'
|
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ export default defineConfig({
|
||||||
target: backendUrl,
|
target: backendUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/install.sh': {
|
||||||
|
target: backendUrl,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/install.ps1': {
|
||||||
|
target: backendUrl,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
'/download': {
|
'/download': {
|
||||||
target: backendUrl,
|
target: backendUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|
|
||||||
110
internal/api/unified_agent.go
Normal file
110
internal/api/unified_agent.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleDownloadUnifiedInstallScript serves the universal install.sh script
|
||||||
|
func (r *Router) handleDownloadUnifiedInstallScript(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(r.config.AppRoot, "scripts", "install.sh")
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||||
|
log.Error().Str("path", scriptPath).Msg("Unified install script not found")
|
||||||
|
http.Error(w, "Install script not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/x-shellscript")
|
||||||
|
w.Header().Set("Content-Disposition", "inline; filename=\"install.sh\"")
|
||||||
|
http.ServeFile(w, req, scriptPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDownloadUnifiedInstallScriptPS serves the universal install.ps1 script
|
||||||
|
func (r *Router) handleDownloadUnifiedInstallScriptPS(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(r.config.AppRoot, "scripts", "install.ps1")
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||||
|
log.Error().Str("path", scriptPath).Msg("Unified PowerShell install script not found")
|
||||||
|
http.Error(w, "Install script not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
w.Header().Set("Content-Disposition", "inline; filename=\"install.ps1\"")
|
||||||
|
http.ServeFile(w, req, scriptPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDownloadUnifiedAgent serves the pulse-agent binary
|
||||||
|
func (r *Router) handleDownloadUnifiedAgent(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, we only have the locally built binary.
|
||||||
|
// In production, this would look up the correct binary for the requested OS/Arch.
|
||||||
|
// Query params: ?os=linux&arch=amd64
|
||||||
|
|
||||||
|
osName := req.URL.Query().Get("os")
|
||||||
|
arch := req.URL.Query().Get("arch")
|
||||||
|
|
||||||
|
if osName == "" {
|
||||||
|
osName = "linux" // Default
|
||||||
|
}
|
||||||
|
if arch == "" {
|
||||||
|
arch = "amd64" // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize OS
|
||||||
|
osName = strings.ToLower(osName)
|
||||||
|
if osName == "darwin" {
|
||||||
|
osName = "macos"
|
||||||
|
}
|
||||||
|
|
||||||
|
// In dev mode, we just serve the binary we built in the root
|
||||||
|
// In prod, we'd look in a dist folder
|
||||||
|
binaryName := "pulse-agent"
|
||||||
|
if osName == "windows" {
|
||||||
|
binaryName = "pulse-agent.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the binary
|
||||||
|
// 1. Check root (dev)
|
||||||
|
binaryPath := filepath.Join(r.config.AppRoot, binaryName)
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
// 2. Check dist folder (prod/build)
|
||||||
|
binaryPath = filepath.Join(r.config.AppRoot, "dist", fmt.Sprintf("%s-%s", osName, arch), binaryName)
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
// Fallback for dev: just serve the root binary regardless of requested OS/Arch
|
||||||
|
// This allows testing the flow even if cross-compilation hasn't happened
|
||||||
|
binaryPath = filepath.Join(r.config.AppRoot, "pulse-agent")
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
log.Error().Str("path", binaryPath).Msg("Unified agent binary not found")
|
||||||
|
http.Error(w, "Agent binary not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", binaryName))
|
||||||
|
http.ServeFile(w, req, binaryPath)
|
||||||
|
}
|
||||||
|
|
@ -77,6 +77,7 @@ type Config struct {
|
||||||
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
|
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
|
||||||
ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"`
|
ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"`
|
||||||
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
||||||
|
AppRoot string `json:"-"` // Root directory of the application (where binary lives)
|
||||||
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://192.168.1.100:7655)
|
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://192.168.1.100:7655)
|
||||||
|
|
||||||
// Proxmox VE connections
|
// Proxmox VE connections
|
||||||
|
|
@ -530,6 +531,7 @@ func Load() (*Config, error) {
|
||||||
FrontendPort: 7655,
|
FrontendPort: 7655,
|
||||||
ConfigPath: dataDir,
|
ConfigPath: dataDir,
|
||||||
DataPath: dataDir,
|
DataPath: dataDir,
|
||||||
|
AppRoot: detectAppRoot(),
|
||||||
ConcurrentPolling: true,
|
ConcurrentPolling: true,
|
||||||
ConnectionTimeout: 60 * time.Second,
|
ConnectionTimeout: 60 * time.Second,
|
||||||
MetricsRetentionDays: 7,
|
MetricsRetentionDays: 7,
|
||||||
|
|
|
||||||
37
internal/config/detect_root.go
Normal file
37
internal/config/detect_root.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// detectAppRoot attempts to find the application root directory
|
||||||
|
func detectAppRoot() string {
|
||||||
|
// 1. Check environment variable
|
||||||
|
if root := os.Getenv("PULSE_APP_ROOT"); root != "" {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get executable path
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
// If running via "go run", executable is in /tmp, which isn't helpful for finding source files
|
||||||
|
// But in production, it's correct.
|
||||||
|
// Check if we are in a temp dir (go run)
|
||||||
|
if strings.Contains(exe, os.TempDir()) || strings.Contains(exe, "/var/folders/") {
|
||||||
|
// Fallback to current working directory
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
return cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath.Dir(exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to current working directory
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
return cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
return "."
|
||||||
|
}
|
||||||
BIN
pulse-agent
Executable file
BIN
pulse-agent
Executable file
Binary file not shown.
|
|
@ -47,13 +47,14 @@ else
|
||||||
openssl rand -base64 32 > "$DEV_DIR/.encryption.key"
|
openssl rand -base64 32 > "$DEV_DIR/.encryption.key"
|
||||||
chmod 600 "$DEV_DIR/.encryption.key"
|
chmod 600 "$DEV_DIR/.encryption.key"
|
||||||
echo "✓ Generated dev encryption key at $DEV_DIR/.encryption.key"
|
echo "✓ Generated dev encryption key at $DEV_DIR/.encryption.key"
|
||||||
|
|
||||||
|
# Remove encrypted artifacts that rely on the missing/old key
|
||||||
|
find "$DEV_DIR" -maxdepth 1 -type f -name 'nodes.enc*' -exec rm -f {} \;
|
||||||
|
rm -f "$DEV_DIR/email.enc" "$DEV_DIR/webhooks.enc"
|
||||||
|
echo "✓ Cleared encrypted artifacts (new key generated)"
|
||||||
else
|
else
|
||||||
echo "✓ Reusing existing dev encryption key"
|
echo "✓ Reusing existing dev encryption key"
|
||||||
fi
|
fi
|
||||||
# Remove encrypted artifacts that rely on the missing production key
|
|
||||||
find "$DEV_DIR" -maxdepth 1 -type f -name 'nodes.enc*' -exec rm -f {} \;
|
|
||||||
rm -f "$DEV_DIR/email.enc" "$DEV_DIR/webhooks.enc"
|
|
||||||
echo "✓ Cleared encrypted production artifacts from dev config"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy nodes configuration - WITH VALIDATION
|
# Copy nodes configuration - WITH VALIDATION
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue