feat: add self-update capability to standalone pulse-host-agent
The standalone pulse-host-agent was missing self-update functionality that existed in pulse-docker-agent and the unified pulse-agent. Changes: - Add agentupdate integration to pulse-host-agent - Add --no-auto-update flag and PULSE_NO_AUTO_UPDATE env var - Update Windows service to use errgroup pattern with auto-updater - Move version from internal/hostagent to main package for ldflags Related to #737
This commit is contained in:
parent
b0ff539fcc
commit
c104ceb19e
3 changed files with 123 additions and 48 deletions
|
|
@ -10,11 +10,24 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentupdate"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Version is the semantic version of the agent, set at build time via ldflags
|
||||||
|
Version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds the configuration for the standalone host agent
|
||||||
|
type Config struct {
|
||||||
|
HostConfig hostagent.Config
|
||||||
|
DisableAutoUpdate bool
|
||||||
|
}
|
||||||
|
|
||||||
type multiValue []string
|
type multiValue []string
|
||||||
|
|
||||||
func (m *multiValue) String() string {
|
func (m *multiValue) String() string {
|
||||||
|
|
@ -28,11 +41,12 @@ func (m *multiValue) Set(value string) error {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := loadConfig()
|
cfg := loadConfig()
|
||||||
|
hostCfg := cfg.HostConfig
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(cfg.LogLevel)
|
zerolog.SetGlobalLevel(hostCfg.LogLevel)
|
||||||
|
|
||||||
logger := zerolog.New(os.Stdout).Level(cfg.LogLevel).With().Timestamp().Logger()
|
logger := zerolog.New(os.Stdout).Level(hostCfg.LogLevel).With().Timestamp().Logger()
|
||||||
cfg.Logger = &logger
|
hostCfg.Logger = &logger
|
||||||
|
|
||||||
// Check if we should run as a Windows service
|
// Check if we should run as a Windows service
|
||||||
if err := runAsWindowsService(cfg, logger); err != nil {
|
if err := runAsWindowsService(cfg, logger); err != nil {
|
||||||
|
|
@ -41,28 +55,55 @@ func main() {
|
||||||
|
|
||||||
// If runAsWindowsService returns nil without error, we're not running as a service
|
// If runAsWindowsService returns nil without error, we're not running as a service
|
||||||
// or we're on a non-Windows platform, so run normally
|
// or we're on a non-Windows platform, so run normally
|
||||||
agent, err := hostagent.New(cfg)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal().Err(err).Msg("failed to initialise host agent")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
logger.Info().
|
logger.Info().
|
||||||
Str("pulse_url", cfg.PulseURL).
|
Str("version", Version).
|
||||||
Str("agent_id", cfg.AgentID).
|
Str("pulse_url", hostCfg.PulseURL).
|
||||||
Dur("interval", cfg.Interval).
|
Str("agent_id", hostCfg.AgentID).
|
||||||
|
Dur("interval", hostCfg.Interval).
|
||||||
|
Bool("auto_update", !cfg.DisableAutoUpdate).
|
||||||
Msg("Starting Pulse host agent")
|
Msg("Starting Pulse host agent")
|
||||||
|
|
||||||
if err := agent.Run(ctx); err != nil && err != context.Canceled {
|
// Start Auto-Updater
|
||||||
|
updater := agentupdate.New(agentupdate.Config{
|
||||||
|
PulseURL: hostCfg.PulseURL,
|
||||||
|
APIToken: hostCfg.APIToken,
|
||||||
|
AgentName: "pulse-host-agent",
|
||||||
|
CurrentVersion: Version,
|
||||||
|
CheckInterval: 1 * time.Hour,
|
||||||
|
InsecureSkipVerify: hostCfg.InsecureSkipVerify,
|
||||||
|
Logger: &logger,
|
||||||
|
Disabled: cfg.DisableAutoUpdate,
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
updater.RunLoop(ctx)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the host agent
|
||||||
|
agent, err := hostagent.New(hostCfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("failed to initialise host agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
return agent.Run(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil && err != context.Canceled {
|
||||||
logger.Fatal().Err(err).Msg("host agent terminated with error")
|
logger.Fatal().Err(err).Msg("host agent terminated with error")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Msg("Host agent stopped")
|
logger.Info().Msg("Host agent stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() hostagent.Config {
|
func loadConfig() Config {
|
||||||
envURL := utils.GetenvTrim("PULSE_URL")
|
envURL := utils.GetenvTrim("PULSE_URL")
|
||||||
envToken := utils.GetenvTrim("PULSE_TOKEN")
|
envToken := utils.GetenvTrim("PULSE_TOKEN")
|
||||||
envInterval := utils.GetenvTrim("PULSE_INTERVAL")
|
envInterval := utils.GetenvTrim("PULSE_INTERVAL")
|
||||||
|
|
@ -72,6 +113,7 @@ func loadConfig() hostagent.Config {
|
||||||
envTags := utils.GetenvTrim("PULSE_TAGS")
|
envTags := utils.GetenvTrim("PULSE_TAGS")
|
||||||
envRunOnce := utils.GetenvTrim("PULSE_ONCE")
|
envRunOnce := utils.GetenvTrim("PULSE_ONCE")
|
||||||
envLogLevel := utils.GetenvTrim("LOG_LEVEL")
|
envLogLevel := utils.GetenvTrim("LOG_LEVEL")
|
||||||
|
envNoAutoUpdate := utils.GetenvTrim("PULSE_NO_AUTO_UPDATE")
|
||||||
|
|
||||||
defaultInterval := 30 * time.Second
|
defaultInterval := 30 * time.Second
|
||||||
if envInterval != "" {
|
if envInterval != "" {
|
||||||
|
|
@ -87,6 +129,7 @@ func loadConfig() hostagent.Config {
|
||||||
agentIDFlag := flag.String("agent-id", envAgentID, "Override agent identifier")
|
agentIDFlag := flag.String("agent-id", envAgentID, "Override agent identifier")
|
||||||
insecureFlag := flag.Bool("insecure", utils.ParseBool(envInsecure), "Skip TLS certificate verification")
|
insecureFlag := flag.Bool("insecure", utils.ParseBool(envInsecure), "Skip TLS certificate verification")
|
||||||
runOnceFlag := flag.Bool("once", utils.ParseBool(envRunOnce), "Collect and send a single report, then exit")
|
runOnceFlag := flag.Bool("once", utils.ParseBool(envRunOnce), "Collect and send a single report, then exit")
|
||||||
|
noAutoUpdateFlag := flag.Bool("no-auto-update", utils.ParseBool(envNoAutoUpdate), "Disable automatic updates")
|
||||||
showVersion := flag.Bool("version", false, "Print the agent version and exit")
|
showVersion := flag.Bool("version", false, "Print the agent version and exit")
|
||||||
logLevelFlag := flag.String("log-level", defaultLogLevel(envLogLevel), "Log level: debug, info, warn, error")
|
logLevelFlag := flag.String("log-level", defaultLogLevel(envLogLevel), "Log level: debug, info, warn, error")
|
||||||
|
|
||||||
|
|
@ -96,7 +139,7 @@ func loadConfig() hostagent.Config {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println(hostagent.Version)
|
fmt.Println(Version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,16 +167,19 @@ func loadConfig() hostagent.Config {
|
||||||
|
|
||||||
tags := gatherTags(envTags, tagFlags)
|
tags := gatherTags(envTags, tagFlags)
|
||||||
|
|
||||||
return hostagent.Config{
|
return Config{
|
||||||
PulseURL: pulseURL,
|
HostConfig: hostagent.Config{
|
||||||
APIToken: token,
|
PulseURL: pulseURL,
|
||||||
Interval: interval,
|
APIToken: token,
|
||||||
HostnameOverride: strings.TrimSpace(*hostnameFlag),
|
Interval: interval,
|
||||||
AgentID: strings.TrimSpace(*agentIDFlag),
|
HostnameOverride: strings.TrimSpace(*hostnameFlag),
|
||||||
Tags: tags,
|
AgentID: strings.TrimSpace(*agentIDFlag),
|
||||||
InsecureSkipVerify: *insecureFlag,
|
Tags: tags,
|
||||||
RunOnce: *runOnceFlag,
|
InsecureSkipVerify: *insecureFlag,
|
||||||
LogLevel: logLevel,
|
RunOnce: *runOnceFlag,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
},
|
||||||
|
DisableAutoUpdate: *noAutoUpdateFlag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runAsWindowsService is a no-op on non-Windows platforms
|
// runAsWindowsService is a no-op on non-Windows platforms
|
||||||
func runAsWindowsService(_ hostagent.Config, _ zerolog.Logger) error {
|
func runAsWindowsService(_ Config, _ zerolog.Logger) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,16 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentupdate"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
"github.com/rcourtman/pulse-go-rewrite/internal/hostagent"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
"golang.org/x/sys/windows/svc"
|
"golang.org/x/sys/windows/svc"
|
||||||
"golang.org/x/sys/windows/svc/eventlog"
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type windowsService struct {
|
type windowsService struct {
|
||||||
cfg hostagent.Config
|
cfg Config
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
eventLog *eventlog.Log
|
eventLog *eventlog.Log
|
||||||
}
|
}
|
||||||
|
|
@ -29,35 +31,63 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha
|
||||||
ws.eventLog.Info(1, "Pulse Host Agent service starting")
|
ws.eventLog.Info(1, "Pulse Host Agent service starting")
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, err := hostagent.New(ws.cfg)
|
hostCfg := ws.cfg.HostConfig
|
||||||
if err != nil {
|
hostCfg.Logger = &ws.logger
|
||||||
ws.logger.Error().Err(err).Msg("Failed to create host agent")
|
|
||||||
changes <- svc.Status{State: svc.Stopped}
|
|
||||||
return true, 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Start the agent in a goroutine
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
errChan := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
ws.logger.Info().
|
|
||||||
Str("pulse_url", ws.cfg.PulseURL).
|
|
||||||
Str("agent_id", ws.cfg.AgentID).
|
|
||||||
Dur("interval", ws.cfg.Interval).
|
|
||||||
Msg("Starting Pulse host agent as Windows service")
|
|
||||||
|
|
||||||
if err := agent.Run(ctx); err != nil && err != context.Canceled {
|
// Start Auto-Updater
|
||||||
errChan <- err
|
updater := agentupdate.New(agentupdate.Config{
|
||||||
|
PulseURL: hostCfg.PulseURL,
|
||||||
|
APIToken: hostCfg.APIToken,
|
||||||
|
AgentName: "pulse-host-agent",
|
||||||
|
CurrentVersion: Version,
|
||||||
|
CheckInterval: 1 * time.Hour,
|
||||||
|
InsecureSkipVerify: hostCfg.InsecureSkipVerify,
|
||||||
|
Logger: &ws.logger,
|
||||||
|
Disabled: ws.cfg.DisableAutoUpdate,
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
updater.RunLoop(ctx)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the host agent
|
||||||
|
agent, err := hostagent.New(hostCfg)
|
||||||
|
if err != nil {
|
||||||
|
ws.logger.Error().Err(err).Msg("Failed to create host agent")
|
||||||
|
if ws.eventLog != nil {
|
||||||
|
ws.eventLog.Error(1, fmt.Sprintf("Failed to create host agent: %v", err))
|
||||||
}
|
}
|
||||||
close(errChan)
|
changes <- svc.Status{State: svc.Stopped}
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
ws.logger.Info().
|
||||||
|
Str("version", Version).
|
||||||
|
Str("pulse_url", hostCfg.PulseURL).
|
||||||
|
Str("agent_id", hostCfg.AgentID).
|
||||||
|
Dur("interval", hostCfg.Interval).
|
||||||
|
Bool("auto_update", !ws.cfg.DisableAutoUpdate).
|
||||||
|
Msg("Starting Pulse host agent as Windows service")
|
||||||
|
return agent.Run(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Channel to receive errgroup completion
|
||||||
|
doneChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
doneChan <- g.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
ws.logger.Info().Msg("Host agent service is running")
|
ws.logger.Info().Msg("Host agent service is running")
|
||||||
if ws.eventLog != nil {
|
if ws.eventLog != nil {
|
||||||
ws.eventLog.Info(1, fmt.Sprintf("Pulse Host Agent started successfully (URL: %s, Interval: %s)", ws.cfg.PulseURL, ws.cfg.Interval))
|
ws.eventLog.Info(1, fmt.Sprintf("Pulse Host Agent started successfully (URL: %s, Interval: %s)", hostCfg.PulseURL, hostCfg.Interval))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service control loop
|
// Service control loop
|
||||||
|
|
@ -79,8 +109,8 @@ loop:
|
||||||
default:
|
default:
|
||||||
ws.logger.Warn().Uint32("command", uint32(c.Cmd)).Msg("Unexpected service control command")
|
ws.logger.Warn().Uint32("command", uint32(c.Cmd)).Msg("Unexpected service control command")
|
||||||
}
|
}
|
||||||
case err := <-errChan:
|
case err := <-doneChan:
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
ws.logger.Error().Err(err).Msg("Agent error")
|
ws.logger.Error().Err(err).Msg("Agent error")
|
||||||
if ws.eventLog != nil {
|
if ws.eventLog != nil {
|
||||||
ws.eventLog.Error(1, fmt.Sprintf("Pulse Host Agent error: %v", err))
|
ws.eventLog.Error(1, fmt.Sprintf("Pulse Host Agent error: %v", err))
|
||||||
|
|
@ -97,7 +127,7 @@ loop:
|
||||||
defer shutdownTimeout.Stop()
|
defer shutdownTimeout.Stop()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-errChan:
|
case <-doneChan:
|
||||||
ws.logger.Info().Msg("Agent stopped gracefully")
|
ws.logger.Info().Msg("Agent stopped gracefully")
|
||||||
if ws.eventLog != nil {
|
if ws.eventLog != nil {
|
||||||
ws.eventLog.Info(1, "Pulse Host Agent stopped gracefully")
|
ws.eventLog.Info(1, "Pulse Host Agent stopped gracefully")
|
||||||
|
|
@ -113,7 +143,7 @@ loop:
|
||||||
return false, 0
|
return false, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAsWindowsService(cfg hostagent.Config, logger zerolog.Logger) error {
|
func runAsWindowsService(cfg Config, logger zerolog.Logger) error {
|
||||||
// Check if we're running as a Windows service
|
// Check if we're running as a Windows service
|
||||||
isService, err := svc.IsWindowsService()
|
isService, err := svc.IsWindowsService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue