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

Commit bd2fd8d

Browse files
Merge pull request #8 from PatchMon/feature/websock
Initial Docker integration
2 parents a127d81 + df18acb commit bd2fd8d

17 files changed

Lines changed: 1276 additions & 19 deletions

File tree

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
BINARY_NAME=patchmon-agent
55
BUILD_DIR=build
66
# Use hardcoded version instead of git tags
7-
VERSION=1.3.0
7+
VERSION=1.3.1
88
# Strip debug info and set version variable
99
LDFLAGS=-ldflags "-s -w -X patchmon-agent/internal/version.Version=$(VERSION)"
1010
# Disable VCS stamping
@@ -27,17 +27,17 @@ all: build
2727
build:
2828
@echo "Building $(BINARY_NAME)..."
2929
@mkdir -p $(BUILD_DIR)
30-
@$(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
30+
@CGO_ENABLED=0 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
3131

3232
# Build for multiple architectures
3333
.PHONY: build-all
3434
build-all:
3535
@echo "Building for multiple architectures..."
3636
@mkdir -p $(BUILD_DIR)
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
37+
@GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-amd64 ./cmd/patchmon-agent
38+
@GOOS=linux GOARCH=386 CGO_ENABLED=0 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-386 ./cmd/patchmon-agent
39+
@GOOS=linux GOARCH=arm64 CGO_ENABLED=0 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
40+
@GOOS=linux GOARCH=arm CGO_ENABLED=0 $(GO_CMD) build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm ./cmd/patchmon-agent
4141

4242
# Install dependencies
4343
.PHONY: deps

cmd/patchmon-agent/commands/report.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
"patchmon-agent/internal/client"
99
"patchmon-agent/internal/hardware"
10+
"patchmon-agent/internal/integrations"
11+
"patchmon-agent/internal/integrations/docker"
1012
"patchmon-agent/internal/network"
1113
"patchmon-agent/internal/packages"
1214
"patchmon-agent/internal/repositories"
@@ -213,6 +215,82 @@ func sendReport() error {
213215
}
214216
}
215217

218+
// Collect and send integration data (Docker, etc.) separately
219+
// This ensures failures in integrations don't affect core system reporting
220+
sendIntegrationData()
221+
216222
logger.Debug("Report process completed")
217223
return nil
218224
}
225+
226+
// sendIntegrationData collects and sends data from integrations (Docker, etc.)
227+
func sendIntegrationData() {
228+
logger.Debug("Starting integration data collection")
229+
230+
// Create integration manager
231+
integrationMgr := integrations.NewManager(logger)
232+
233+
// Register available integrations
234+
integrationMgr.Register(docker.New(logger))
235+
// Future: integrationMgr.Register(proxmox.New(logger))
236+
// Future: integrationMgr.Register(kubernetes.New(logger))
237+
238+
// Discover and collect from all available integrations
239+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
240+
defer cancel()
241+
242+
integrationData := integrationMgr.CollectAll(ctx)
243+
244+
if len(integrationData) == 0 {
245+
logger.Debug("No integration data to send")
246+
return
247+
}
248+
249+
// Get system info for integration payloads
250+
systemDetector := system.New(logger)
251+
hostname, _ := systemDetector.GetHostname()
252+
machineID := systemDetector.GetMachineID()
253+
254+
// Create HTTP client
255+
httpClient := client.New(cfgManager, logger)
256+
257+
// Send Docker data if available
258+
if dockerData, exists := integrationData["docker"]; exists && dockerData.Error == "" {
259+
sendDockerData(httpClient, dockerData, hostname, machineID)
260+
}
261+
262+
// Future: Send other integration data here
263+
}
264+
265+
// sendDockerData sends Docker integration data to server
266+
func sendDockerData(httpClient *client.Client, integrationData *models.IntegrationData, hostname, machineID string) {
267+
// Extract Docker data from integration data
268+
dockerData, ok := integrationData.Data.(*models.DockerData)
269+
if !ok {
270+
logger.Warn("Failed to extract Docker data from integration")
271+
return
272+
}
273+
274+
payload := &models.DockerPayload{
275+
DockerData: *dockerData,
276+
Hostname: hostname,
277+
MachineID: machineID,
278+
AgentVersion: version.Version,
279+
}
280+
281+
logger.Info("Sending Docker data to server...")
282+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
283+
defer cancel()
284+
285+
response, err := httpClient.SendDockerData(ctx, payload)
286+
if err != nil {
287+
logger.WithError(err).Warn("Failed to send Docker data (will retry on next report)")
288+
return
289+
}
290+
291+
logger.WithFields(logrus.Fields{
292+
"containers": response.ContainersReceived,
293+
"images": response.ImagesReceived,
294+
"updates": response.UpdatesFound,
295+
}).Info("Docker data sent successfully")
296+
}

cmd/patchmon-agent/commands/serve.go

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package commands
22

33
import (
44
"context"
5+
"crypto/tls"
56
"encoding/json"
67
"net/http"
78
"strings"
89
"time"
910

1011
"patchmon-agent/internal/client"
12+
"patchmon-agent/internal/integrations"
13+
"patchmon-agent/internal/integrations/docker"
14+
"patchmon-agent/pkg/models"
1115

1216
"github.com/gorilla/websocket"
1317
"github.com/spf13/cobra"
@@ -46,14 +50,30 @@ func runService() error {
4650
ticker := time.NewTicker(time.Duration(intervalMinutes) * time.Minute)
4751
defer ticker.Stop()
4852

53+
// Send startup ping to notify server that agent has started
54+
logger.Info("🚀 Agent starting up, notifying server...")
55+
if _, err := httpClient.Ping(ctx); err != nil {
56+
logger.WithError(err).Warn("startup ping failed, will retry")
57+
} else {
58+
logger.Info("✅ Startup notification sent to server")
59+
}
60+
4961
// initial report on boot
62+
logger.Info("Sending initial report on startup...")
5063
if err := sendReport(); err != nil {
5164
logger.WithError(err).Warn("initial report failed")
65+
} else {
66+
logger.Info("✅ Initial report sent successfully")
5267
}
5368

5469
// start websocket loop
70+
logger.Info("Establishing WebSocket connection...")
5571
messages := make(chan wsMsg, 10)
56-
go wsLoop(messages)
72+
dockerEvents := make(chan interface{}, 100)
73+
go wsLoop(messages, dockerEvents)
74+
75+
// Start integration monitoring (Docker real-time events, etc.)
76+
startIntegrationMonitoring(ctx, dockerEvents)
5777

5878
for {
5979
select {
@@ -92,17 +112,40 @@ func runService() error {
92112
}
93113
}
94114

115+
// startIntegrationMonitoring starts real-time monitoring for integrations that support it
116+
func startIntegrationMonitoring(ctx context.Context, eventChan chan<- interface{}) {
117+
// Create integration manager
118+
integrationMgr := integrations.NewManager(logger)
119+
120+
// Register integrations
121+
dockerInteg := docker.New(logger)
122+
integrationMgr.Register(dockerInteg)
123+
124+
// Start monitoring for real-time integrations
125+
realtimeIntegrations := integrationMgr.GetRealtimeIntegrations()
126+
for _, integration := range realtimeIntegrations {
127+
logger.WithField("integration", integration.Name()).Info("Starting real-time monitoring")
128+
129+
// Start monitoring in a goroutine
130+
go func(integ integrations.RealtimeIntegration) {
131+
if err := integ.StartMonitoring(ctx, eventChan); err != nil {
132+
logger.WithError(err).Warn("Failed to start integration monitoring")
133+
}
134+
}(integration)
135+
}
136+
}
137+
95138
type wsMsg struct {
96139
kind string
97140
interval int
98141
version string
99142
force bool
100143
}
101144

102-
func wsLoop(out chan<- wsMsg) {
145+
func wsLoop(out chan<- wsMsg, dockerEvents <-chan interface{}) {
103146
backoff := time.Second
104147
for {
105-
if err := connectOnce(out); err != nil {
148+
if err := connectOnce(out, dockerEvents); err != nil {
106149
logger.WithError(err).Warn("ws disconnected; retrying")
107150
}
108151
time.Sleep(backoff)
@@ -112,7 +155,7 @@ func wsLoop(out chan<- wsMsg) {
112155
}
113156
}
114157

115-
func connectOnce(out chan<- wsMsg) error {
158+
func connectOnce(out chan<- wsMsg, dockerEvents <-chan interface{}) error {
116159
server := cfgManager.GetConfig().PatchmonServer
117160
if server == "" {
118161
return nil
@@ -135,7 +178,18 @@ func connectOnce(out chan<- wsMsg) error {
135178
header.Set("X-API-ID", apiID)
136179
header.Set("X-API-KEY", apiKey)
137180

138-
conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
181+
// Configure WebSocket dialer for insecure connections if needed
182+
dialer := websocket.DefaultDialer
183+
if cfgManager.GetConfig().SkipSSLVerify {
184+
dialer = &websocket.Dialer{
185+
TLSClientConfig: &tls.Config{
186+
InsecureSkipVerify: true,
187+
},
188+
}
189+
logger.Warn("⚠️ SSL certificate verification is disabled for WebSocket")
190+
}
191+
192+
conn, _, err := dialer.Dial(wsURL, header)
139193
if err != nil {
140194
return err
141195
}
@@ -157,6 +211,32 @@ func connectOnce(out chan<- wsMsg) error {
157211
})
158212

159213
logger.WithField("url", wsURL).Info("WebSocket connected")
214+
215+
// Create a goroutine to send Docker events through WebSocket
216+
go func() {
217+
for event := range dockerEvents {
218+
if dockerEvent, ok := event.(models.DockerStatusEvent); ok {
219+
eventJSON, err := json.Marshal(map[string]interface{}{
220+
"type": "docker_status",
221+
"event": dockerEvent,
222+
"container_id": dockerEvent.ContainerID,
223+
"name": dockerEvent.Name,
224+
"status": dockerEvent.Status,
225+
"timestamp": dockerEvent.Timestamp,
226+
})
227+
if err != nil {
228+
logger.WithError(err).Warn("Failed to marshal Docker event")
229+
continue
230+
}
231+
232+
if err := conn.WriteMessage(websocket.TextMessage, eventJSON); err != nil {
233+
logger.WithError(err).Debug("Failed to send Docker event via WebSocket")
234+
return
235+
}
236+
}
237+
}
238+
}()
239+
160240
for {
161241
_, data, err := conn.ReadMessage()
162242
if err != nil {

cmd/patchmon-agent/commands/version_update.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"context"
55
"crypto/sha256"
6+
"crypto/tls"
67
"encoding/json"
78
"fmt"
89
"io"
@@ -249,7 +250,20 @@ func getLatestBinaryFromServer() (*ServerVersionResponse, error) {
249250
req.Header.Set("X-API-ID", credentials.APIID)
250251
req.Header.Set("X-API-KEY", credentials.APIKey)
251252

252-
resp, err := http.DefaultClient.Do(req)
253+
// Configure HTTP client for insecure SSL if needed
254+
httpClient := http.DefaultClient
255+
if cfg.SkipSSLVerify {
256+
logger.Warn("⚠️ SSL certificate verification is disabled for binary download")
257+
httpClient = &http.Client{
258+
Transport: &http.Transport{
259+
TLSClientConfig: &tls.Config{
260+
InsecureSkipVerify: true,
261+
},
262+
},
263+
}
264+
}
265+
266+
resp, err := httpClient.Do(req)
253267
if err != nil {
254268
return nil, err
255269
}

go.mod

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module patchmon-agent
33
go 1.25
44

55
require (
6+
github.com/docker/docker v28.5.1+incompatible
67
github.com/go-resty/resty/v2 v2.16.5
78
github.com/gorilla/websocket v1.5.3
89
github.com/shirou/gopsutil/v4 v4.25.9
@@ -14,14 +15,31 @@ require (
1415
)
1516

1617
require (
18+
github.com/Microsoft/go-winio v0.6.2 // indirect
19+
github.com/containerd/errdefs v1.0.0 // indirect
20+
github.com/containerd/errdefs/pkg v0.3.0 // indirect
21+
github.com/containerd/log v0.1.0 // indirect
1722
github.com/davecgh/go-spew v1.1.1 // indirect
23+
github.com/distribution/reference v0.6.0 // indirect
24+
github.com/docker/go-connections v0.6.0 // indirect
25+
github.com/docker/go-units v0.5.0 // indirect
1826
github.com/ebitengine/purego v0.9.0 // indirect
27+
github.com/felixge/httpsnoop v1.0.4 // indirect
1928
github.com/fsnotify/fsnotify v1.9.0 // indirect
29+
github.com/go-logr/logr v1.4.3 // indirect
30+
github.com/go-logr/stdr v1.2.2 // indirect
2031
github.com/go-ole/go-ole v1.2.6 // indirect
2132
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2233
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2334
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
35+
github.com/moby/docker-image-spec v1.3.1 // indirect
36+
github.com/moby/sys/atomicwriter v0.1.0 // indirect
37+
github.com/moby/term v0.5.2 // indirect
38+
github.com/morikuni/aec v1.0.0 // indirect
39+
github.com/opencontainers/go-digest v1.0.0 // indirect
40+
github.com/opencontainers/image-spec v1.1.1 // indirect
2441
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
42+
github.com/pkg/errors v0.9.1 // indirect
2543
github.com/pmezard/go-difflib v1.0.0 // indirect
2644
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
2745
github.com/sagikazarmark/locafero v0.12.0 // indirect
@@ -32,10 +50,16 @@ require (
3250
github.com/tklauser/go-sysconf v0.3.15 // indirect
3351
github.com/tklauser/numcpus v0.10.0 // indirect
3452
github.com/yusufpapurcu/wmi v1.2.4 // indirect
53+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
54+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
55+
go.opentelemetry.io/otel v1.38.0 // indirect
56+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
57+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
58+
go.opentelemetry.io/otel/trace v1.38.0 // indirect
3559
go.yaml.in/yaml/v3 v3.0.4 // indirect
3660
golang.org/x/net v0.44.0 // indirect
3761
golang.org/x/sys v0.36.0 // indirect
3862
golang.org/x/text v0.29.0 // indirect
39-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
4063
gopkg.in/yaml.v3 v3.0.1 // indirect
64+
gotest.tools/v3 v3.5.2 // indirect
4165
)

0 commit comments

Comments
 (0)