Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit a127d81

Browse files
Merge pull request #6 from PatchMon/feature/websock
Fixed auto update detection and mechanism to restart its own service …
2 parents 0aaedf3 + 0fab3b9 commit a127d81

5 files changed

Lines changed: 126 additions & 83 deletions

File tree

Makefile

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ BUILD_FLAGS=-buildvcs=false
1313
# Go variables
1414
GOBASE=$(shell pwd)
1515
GOBIN=$(GOBASE)/$(BUILD_DIR)
16+
# Use full path to go binary to avoid PATH issues when running as root
17+
GO_CMD=/usr/local/bin/go/bin/go
18+
# Use full path to golangci-lint binary to avoid PATH issues when running as root
19+
GOLANGCI_LINT_CMD=/home/ibrahim/go/bin/golangci-lint
1620

1721
# Default target
1822
.PHONY: all
@@ -23,49 +27,49 @@ all: build
2327
build:
2428
@echo "Building $(BINARY_NAME)..."
2529
@mkdir -p $(BUILD_DIR)
26-
@go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
30+
@$(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
2731

2832
# Build for multiple architectures
2933
.PHONY: build-all
3034
build-all:
3135
@echo "Building for multiple architectures..."
3236
@mkdir -p $(BUILD_DIR)
33-
@GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-amd64 ./cmd/patchmon-agent
34-
@GOOS=linux GOARCH=386 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-386 ./cmd/patchmon-agent
35-
@GOOS=linux GOARCH=arm64 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
36-
@GOOS=linux GOARCH=arm go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm ./cmd/patchmon-agent
37+
@GOOS=linux GOARCH=amd64 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-amd64 ./cmd/patchmon-agent
38+
@GOOS=linux GOARCH=386 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-386 ./cmd/patchmon-agent
39+
@GOOS=linux GOARCH=arm64 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
40+
@GOOS=linux GOARCH=arm $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm ./cmd/patchmon-agent
3741

3842
# Install dependencies
3943
.PHONY: deps
4044
deps:
4145
@echo "Installing dependencies..."
42-
@go mod download
43-
@go mod tidy
46+
@$(GO_CMD) mod download
47+
@$(GO_CMD) mod tidy
4448

4549
# Run tests
4650
.PHONY: test
4751
test:
4852
@echo "Running tests..."
49-
@go test -v ./...
53+
@$(GO_CMD) test -v ./...
5054

5155
# Run tests with coverage
5256
.PHONY: test-coverage
5357
test-coverage:
5458
@echo "Running tests with coverage..."
55-
@go test -v -coverprofile=coverage.out ./...
56-
@go tool cover -html=coverage.out -o coverage.html
59+
@$(GO_CMD) test -v -coverprofile=coverage.out ./...
60+
@$(GO_CMD) tool cover -html=coverage.out -o coverage.html
5761

5862
# Format code
5963
.PHONY: fmt
6064
fmt:
6165
@echo "Formatting code..."
62-
@go fmt ./...
66+
@$(GO_CMD) fmt ./...
6367

6468
# Lint code
6569
.PHONY: lint
6670
lint:
6771
@echo "Linting code..."
68-
@golangci-lint run
72+
@PATH="/usr/local/bin/go/bin:$$PATH" GOFLAGS="$(BUILD_FLAGS)" $(GOLANGCI_LINT_CMD) run
6973

7074
# Clean build artifacts
7175
.PHONY: clean

cmd/patchmon-agent/commands/report.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func sendReport() error {
177177
logger.Info("Report sent successfully")
178178
logger.WithField("count", response.PackagesProcessed).Info("Processed packages")
179179

180-
// Handle agent auto-update
180+
// Handle agent auto-update (server-initiated)
181181
if response.AutoUpdate != nil && response.AutoUpdate.ShouldUpdate {
182182
logger.WithFields(logrus.Fields{
183183
"current": response.AutoUpdate.CurrentVersion,
@@ -191,6 +191,26 @@ func sendReport() error {
191191
} else {
192192
logger.Info("PatchMon agent update completed successfully")
193193
}
194+
} else {
195+
// Proactive update check after report
196+
logger.Info("Checking for agent updates...")
197+
versionInfo, err := getServerVersionInfo()
198+
if err != nil {
199+
logger.WithError(err).Warn("Failed to check for updates after report")
200+
} else if versionInfo.HasUpdate {
201+
logger.WithFields(logrus.Fields{
202+
"current": versionInfo.CurrentVersion,
203+
"latest": versionInfo.LatestVersion,
204+
}).Info("Update available, automatically updating...")
205+
206+
if err := updateAgent(); err != nil {
207+
logger.WithError(err).Warn("PatchMon agent update failed, but data was sent successfully")
208+
} else {
209+
logger.Info("PatchMon agent update completed successfully")
210+
}
211+
} else {
212+
logger.WithField("version", versionInfo.CurrentVersion).Debug("Agent is up to date")
213+
}
194214
}
195215

196216
logger.Debug("Report process completed")

cmd/patchmon-agent/commands/root.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package commands
22

33
import (
4-
"fmt"
5-
"os"
6-
"path/filepath"
4+
"fmt"
5+
"os"
6+
"path/filepath"
77

8-
"patchmon-agent/internal/config"
9-
"patchmon-agent/internal/constants"
10-
"patchmon-agent/internal/version"
8+
"patchmon-agent/internal/config"
9+
"patchmon-agent/internal/constants"
10+
"patchmon-agent/internal/version"
1111

12-
"github.com/sirupsen/logrus"
13-
"github.com/spf13/cobra"
14-
lumberjack "gopkg.in/natefinch/lumberjack.v2"
12+
"github.com/sirupsen/logrus"
13+
"github.com/spf13/cobra"
14+
lumberjack "gopkg.in/natefinch/lumberjack.v2"
1515
)
1616

1717
var (
@@ -53,7 +53,7 @@ func init() {
5353
rootCmd.AddCommand(pingCmd)
5454
rootCmd.AddCommand(configCmd)
5555
rootCmd.AddCommand(checkVersionCmd)
56-
rootCmd.AddCommand(updateAgentCmd)
56+
rootCmd.AddCommand(updateAgentCmd)
5757
rootCmd.AddCommand(diagnosticsCmd)
5858
rootCmd.AddCommand(uninstallCmd)
5959
}
@@ -68,18 +68,18 @@ func initialiseAgent() {
6868
TimestampFormat: "2006-01-02T15:04:05",
6969
})
7070

71-
// Initialise configuration manager
72-
cfgManager = config.New()
73-
cfgManager.SetConfigFile(configFile)
74-
75-
// Load config early to determine log file path
76-
_ = cfgManager.LoadConfig()
77-
logFile := cfgManager.GetConfig().LogFile
78-
if logFile == "" {
79-
logFile = config.DefaultLogFile
80-
}
81-
_ = os.MkdirAll(filepath.Dir(logFile), 0755)
82-
logger.SetOutput(&lumberjack.Logger{Filename: logFile, MaxSize: 10, MaxBackups: 5, MaxAge: 14, Compress: true})
71+
// Initialise configuration manager
72+
cfgManager = config.New()
73+
cfgManager.SetConfigFile(configFile)
74+
75+
// Load config early to determine log file path
76+
_ = cfgManager.LoadConfig()
77+
logFile := cfgManager.GetConfig().LogFile
78+
if logFile == "" {
79+
logFile = config.DefaultLogFile
80+
}
81+
_ = os.MkdirAll(filepath.Dir(logFile), 0755)
82+
logger.SetOutput(&lumberjack.Logger{Filename: logFile, MaxSize: 10, MaxBackups: 5, MaxAge: 14, Compress: true})
8383
}
8484

8585
// updateLogLevel sets the logger level based on the flag value

cmd/patchmon-agent/commands/version_update.go

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -28,6 +29,7 @@ type ServerVersionResponse struct {
2829
Size int64 `json:"size"`
2930
Hash string `json:"hash"`
3031
DownloadURL string `json:"downloadUrl"`
32+
BinaryData []byte `json:"-"` // Binary data (not serialized to JSON)
3133
}
3234

3335
type ServerVersionInfo struct {
@@ -81,11 +83,10 @@ func checkVersion() error {
8183
logger.Info("Agent update available!")
8284
fmt.Printf(" Current version: %s\n", currentVersion)
8385
fmt.Printf(" Latest version: %s\n", latestVersion)
84-
fmt.Printf(" Last checked: %s\n", versionInfo.LastChecked)
85-
8686
fmt.Printf("\nTo update, run: patchmon-agent update-agent\n")
8787
} else {
8888
logger.WithField("version", currentVersion).Info("Agent is up to date")
89+
fmt.Printf("Agent is up to date (version %s)\n", currentVersion)
8990
}
9091

9192
return nil
@@ -108,12 +109,12 @@ func updateAgent() error {
108109

109110
logger.WithField("version", binaryInfo.Version).Info("Found latest version")
110111

111-
logger.WithField("url", binaryInfo.DownloadURL).Info("Downloading latest agent...")
112+
logger.Info("Using downloaded agent binary...")
112113

113-
// Download new version
114-
newAgentData, err := downloadBinaryFromServer(binaryInfo.DownloadURL)
115-
if err != nil {
116-
return fmt.Errorf("failed to download new agent: %w", err)
114+
// Use the binary data directly from the server response
115+
newAgentData := binaryInfo.BinaryData
116+
if len(newAgentData) == 0 {
117+
return fmt.Errorf("no binary data received from server")
117118
}
118119

119120
// Create backup of current executable
@@ -148,6 +149,14 @@ func updateAgent() error {
148149

149150
logger.WithField("version", binaryInfo.Version).Info("Agent updated successfully")
150151

152+
// Restart the systemd service to pick up the new binary
153+
logger.Info("Restarting patchmon-agent service...")
154+
if err := restartService(); err != nil {
155+
logger.WithError(err).Warn("Failed to restart service (this is not critical)")
156+
} else {
157+
logger.Info("Service restarted successfully")
158+
}
159+
151160
// Send updated information to PatchMon
152161
logger.Info("Sending updated information to PatchMon...")
153162
if err := sendReport(); err != nil {
@@ -167,7 +176,15 @@ func getServerVersionInfo() (*ServerVersionInfo, error) {
167176
}
168177
cfg := cfgManager.GetConfig()
169178

170-
url := fmt.Sprintf("%s/api/v1/agent/version", cfg.PatchmonServer)
179+
// Load credentials for API authentication
180+
if err := cfgManager.LoadCredentials(); err != nil {
181+
return nil, fmt.Errorf("failed to load credentials: %w", err)
182+
}
183+
credentials := cfgManager.GetCredentials()
184+
185+
architecture := getArchitecture()
186+
currentVersion := strings.TrimPrefix(version.Version, "v")
187+
url := fmt.Sprintf("%s/api/v1/hosts/agent/version?arch=%s&type=go&currentVersion=%s", cfg.PatchmonServer, architecture, currentVersion)
171188

172189
ctx, cancel := context.WithTimeout(context.Background(), serverTimeout)
173190
defer cancel()
@@ -178,6 +195,8 @@ func getServerVersionInfo() (*ServerVersionInfo, error) {
178195
}
179196

180197
req.Header.Set("User-Agent", fmt.Sprintf("patchmon-agent/%s", version.Version))
198+
req.Header.Set("X-API-ID", credentials.APIID)
199+
req.Header.Set("X-API-KEY", credentials.APIKey)
181200

182201
resp, err := http.DefaultClient.Do(req)
183202
if err != nil {
@@ -209,8 +228,14 @@ func getLatestBinaryFromServer() (*ServerVersionResponse, error) {
209228
}
210229
cfg := cfgManager.GetConfig()
211230

231+
// Load credentials for API authentication
232+
if err := cfgManager.LoadCredentials(); err != nil {
233+
return nil, fmt.Errorf("failed to load credentials: %w", err)
234+
}
235+
credentials := cfgManager.GetCredentials()
236+
212237
architecture := getArchitecture()
213-
url := fmt.Sprintf("%s/api/v1/agent/latest/%s", cfg.PatchmonServer, architecture)
238+
url := fmt.Sprintf("%s/api/v1/hosts/agent/download?arch=%s", cfg.PatchmonServer, architecture)
214239

215240
ctx, cancel := context.WithTimeout(context.Background(), serverTimeout)
216241
defer cancel()
@@ -221,6 +246,8 @@ func getLatestBinaryFromServer() (*ServerVersionResponse, error) {
221246
}
222247

223248
req.Header.Set("User-Agent", fmt.Sprintf("patchmon-agent/%s", version.Version))
249+
req.Header.Set("X-API-ID", credentials.APIID)
250+
req.Header.Set("X-API-KEY", credentials.APIKey)
224251

225252
resp, err := http.DefaultClient.Do(req)
226253
if err != nil {
@@ -236,51 +263,28 @@ func getLatestBinaryFromServer() (*ServerVersionResponse, error) {
236263
return nil, fmt.Errorf("server returned status %d", resp.StatusCode)
237264
}
238265

239-
var binaryInfo ServerVersionResponse
240-
if err := json.NewDecoder(resp.Body).Decode(&binaryInfo); err != nil {
241-
return nil, fmt.Errorf("failed to decode binary info: %w", err)
242-
}
243-
244-
return &binaryInfo, nil
245-
}
246-
247-
// downloadBinaryFromServer downloads a binary from the PatchMon server
248-
func downloadBinaryFromServer(url string) ([]byte, error) {
249-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
250-
defer cancel()
251-
252-
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
266+
// Read the binary data
267+
binaryData, err := io.ReadAll(resp.Body)
253268
if err != nil {
254-
return nil, err
269+
return nil, fmt.Errorf("failed to read binary data: %w", err)
255270
}
256271

257-
req.Header.Set("User-Agent", fmt.Sprintf("patchmon-agent/%s", version.Version))
258-
259-
resp, err := http.DefaultClient.Do(req)
260-
if err != nil {
261-
return nil, err
262-
}
263-
defer func() {
264-
if closeErr := resp.Body.Close(); closeErr != nil {
265-
logger.WithError(closeErr).Debug("Failed to close response body")
266-
}
267-
}()
268-
269-
if resp.StatusCode != http.StatusOK {
270-
return nil, fmt.Errorf("download failed with status %d", resp.StatusCode)
271-
}
272-
273-
data, err := io.ReadAll(resp.Body)
274-
if err != nil {
275-
return nil, fmt.Errorf("failed to read response body: %w", err)
276-
}
277-
278-
return data, nil
272+
// Calculate hash
273+
hash := fmt.Sprintf("%x", sha256.Sum256(binaryData))
274+
275+
return &ServerVersionResponse{
276+
Version: version.Version, // We'll get the actual version from the server later
277+
Architecture: architecture,
278+
Size: int64(len(binaryData)),
279+
Hash: hash,
280+
DownloadURL: url,
281+
BinaryData: binaryData, // Store the binary data directly
282+
}, nil
279283
}
280284

281285
// getArchitecture returns the architecture string for the current platform
282286
func getArchitecture() string {
283-
return fmt.Sprintf("linux-%s", runtime.GOARCH)
287+
return runtime.GOARCH
284288
}
285289

286290
// copyFile copies a file from src to dst
@@ -293,4 +297,19 @@ func copyFile(src, dst string) error {
293297
return os.WriteFile(dst, data, 0755)
294298
}
295299

300+
// restartService restarts the patchmon-agent systemd service
301+
func restartService() error {
302+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
303+
defer cancel()
304+
305+
cmd := exec.CommandContext(ctx, "systemctl", "restart", "patchmon-agent")
306+
output, err := cmd.CombinedOutput()
307+
if err != nil {
308+
return fmt.Errorf("failed to restart service: %w, output: %s", err, string(output))
309+
}
310+
311+
logger.WithField("output", string(output)).Debug("Service restart command completed")
312+
return nil
313+
}
314+
296315
// Removed update-crontab command (cron is no longer used)

internal/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const (
1616
DefaultAPIVersion = "v1"
1717
DefaultConfigFile = "/etc/patchmon/config.yml"
1818
DefaultCredentialsFile = "/etc/patchmon/credentials.yml"
19-
DefaultLogFile = "/etc/patchmon/logs/patchmon-agent.log"
19+
DefaultLogFile = "/etc/patchmon/logs/patchmon-agent.log"
2020
DefaultLogLevel = "info"
2121
CronFilePath = "/etc/cron.d/patchmon-agent"
2222
)

0 commit comments

Comments
 (0)