Skip to content

Commit 8553866

Browse files
Merge pull request #10 from PatchMon/feature/websock
Adding Docker networks and volumes
2 parents cf01b4d + 4c0bf69 commit 8553866

File tree

7 files changed

+290
-26
lines changed

7 files changed

+290
-26
lines changed

cmd/patchmon-agent/commands/report.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,13 @@ func sendDockerData(httpClient *client.Client, integrationData *models.Integrati
278278
AgentVersion: version.Version,
279279
}
280280

281-
logger.Info("Sending Docker data to server...")
281+
logger.WithFields(logrus.Fields{
282+
"containers": len(dockerData.Containers),
283+
"images": len(dockerData.Images),
284+
"volumes": len(dockerData.Volumes),
285+
"networks": len(dockerData.Networks),
286+
"updates": len(dockerData.Updates),
287+
}).Info("Sending Docker data to server...")
282288
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
283289
defer cancel()
284290

@@ -291,6 +297,8 @@ func sendDockerData(httpClient *client.Client, integrationData *models.Integrati
291297
logger.WithFields(logrus.Fields{
292298
"containers": response.ContainersReceived,
293299
"images": response.ImagesReceived,
300+
"volumes": response.VolumesReceived,
301+
"networks": response.NetworksReceived,
294302
"updates": response.UpdatesFound,
295303
}).Info("Docker data sent successfully")
296304
}

internal/integrations/docker/docker.go

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"strings"
78
"sync"
89
"time"
910

@@ -103,6 +104,8 @@ func (d *Integration) Collect(ctx context.Context) (*models.IntegrationData, err
103104
dockerData := &models.DockerData{
104105
Containers: make([]models.DockerContainer, 0),
105106
Images: make([]models.DockerImage, 0),
107+
Volumes: make([]models.DockerVolume, 0),
108+
Networks: make([]models.DockerNetwork, 0),
106109
Updates: make([]models.DockerImageUpdate, 0),
107110
}
108111

@@ -124,6 +127,24 @@ func (d *Integration) Collect(ctx context.Context) (*models.IntegrationData, err
124127
d.logger.WithField("count", len(images)).Info("Collected images")
125128
}
126129

130+
// Collect volumes
131+
volumes, err := d.collectVolumes(ctx)
132+
if err != nil {
133+
d.logger.WithError(err).Warn("Failed to collect volumes")
134+
} else {
135+
dockerData.Volumes = volumes
136+
d.logger.WithField("count", len(volumes)).Info("Collected volumes")
137+
}
138+
139+
// Collect networks
140+
networks, err := d.collectNetworks(ctx)
141+
if err != nil {
142+
d.logger.WithError(err).Warn("Failed to collect networks")
143+
} else {
144+
dockerData.Networks = networks
145+
d.logger.WithField("count", len(networks)).Info("Collected networks")
146+
}
147+
127148
// Collect daemon info
128149
daemonInfo, err := d.collectDaemonInfo(ctx)
129150
if err != nil {
@@ -213,32 +234,47 @@ func determineImageSource(imageName string) string {
213234
return "unknown"
214235
}
215236

216-
// Check for common registries
217-
if len(imageName) > 9 && imageName[:9] == "ghcr.io/" {
218-
return "github"
237+
// Extract domain from image name
238+
parts := strings.SplitN(imageName, "/", 2)
239+
if len(parts) == 1 {
240+
// No domain specified, it's Docker Hub (implicit docker.io)
241+
return "docker-hub"
219242
}
220-
if len(imageName) > 20 && imageName[:20] == "registry.gitlab.com/" {
221-
return "gitlab"
243+
244+
domain := parts[0]
245+
246+
// Check if first part contains a dot or colon (indicates it's a domain)
247+
if !strings.Contains(domain, ".") && !strings.Contains(domain, ":") {
248+
// It's a Docker Hub image with org/repo format (e.g., "library/nginx")
249+
return "docker-hub"
222250
}
223-
if len(imageName) > 4 && imageName[:4] == "gcr." {
251+
252+
// Match known registry domains (inspired by diun)
253+
switch {
254+
case domain == "docker.io":
255+
return "docker-hub"
256+
case domain == "ghcr.io":
257+
return "github"
258+
case domain == "docker.pkg.github.com":
259+
return "github"
260+
case domain == "registry.gitlab.com":
261+
return "gitlab"
262+
case strings.HasPrefix(domain, "gcr.io"):
224263
return "google"
225-
}
226-
if len(imageName) > 4 && imageName[:4] == "quay" {
264+
case strings.Contains(domain, "pkg.dev"): // Google Artifact Registry
265+
return "google"
266+
case domain == "quay.io":
227267
return "quay"
228-
}
229-
230-
// Count slashes to determine if it's a private registry
231-
slashCount := 0
232-
for _, ch := range imageName {
233-
if ch == '/' {
234-
slashCount++
235-
}
236-
}
237-
if slashCount >= 2 {
268+
case domain == "registry.access.redhat.com":
269+
return "redhat"
270+
case strings.Contains(domain, "azurecr.io"):
271+
return "azure"
272+
case strings.Contains(domain, "amazonaws.com"): // ECR
273+
return "aws"
274+
default:
275+
// Private registry
238276
return "private"
239277
}
240-
241-
return "docker-hub"
242278
}
243279

244280
// parseImageName parses image name into repository and tag

internal/integrations/docker/images.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ func (d *Integration) collectImages(ctx context.Context) ([]models.DockerImage,
3939
// Parse image name
4040
repository, tag := parseImageName(repoTag)
4141

42-
// Determine source
43-
source := determineImageSource(repository)
44-
45-
// Get digest
42+
// Get digest first to determine if image is locally built
4643
digest := ""
4744
if len(img.RepoDigests) > 0 {
4845
// Extract just the hash part
@@ -52,6 +49,13 @@ func (d *Integration) collectImages(ctx context.Context) ([]models.DockerImage,
5249
}
5350
}
5451

52+
// Determine source - if no digest, image is locally built
53+
source := determineImageSource(repository)
54+
if len(img.RepoDigests) == 0 || digest == "" {
55+
// No RepoDigests means the image was built locally and never pushed to a registry
56+
source = "local"
57+
}
58+
5559
// Convert created timestamp
5660
var createdAt *time.Time
5761
if img.Created > 0 {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"patchmon-agent/pkg/models"
8+
9+
"github.com/docker/docker/api/types/network"
10+
)
11+
12+
// collectNetworks collects all Docker networks
13+
func (d *Integration) collectNetworks(ctx context.Context) ([]models.DockerNetwork, error) {
14+
// List all networks
15+
networks, err := d.client.NetworkList(ctx, network.ListOptions{})
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to list networks: %w", err)
18+
}
19+
20+
result := make([]models.DockerNetwork, 0, len(networks))
21+
22+
for _, net := range networks {
23+
// Parse IPAM configuration
24+
var ipam *models.DockerIPAM
25+
// Check if IPAM config exists (Config slice length > 0 or Driver is set)
26+
if len(net.IPAM.Config) > 0 || net.IPAM.Driver != "" {
27+
ipam = &models.DockerIPAM{
28+
Driver: net.IPAM.Driver,
29+
Options: net.IPAM.Options,
30+
Config: make([]models.DockerIPAMConfig, 0),
31+
}
32+
33+
for _, ipamConfig := range net.IPAM.Config {
34+
// Convert auxiliary addresses to map
35+
auxAddresses := make(map[string]string)
36+
if ipamConfig.AuxAddress != nil {
37+
for k, v := range ipamConfig.AuxAddress {
38+
auxAddresses[k] = v
39+
}
40+
}
41+
42+
ipamConfigData := models.DockerIPAMConfig{
43+
Subnet: ipamConfig.Subnet,
44+
Gateway: ipamConfig.Gateway,
45+
IPRange: ipamConfig.IPRange,
46+
AuxAddresses: auxAddresses,
47+
}
48+
ipam.Config = append(ipam.Config, ipamConfigData)
49+
}
50+
}
51+
52+
// Count containers attached to this network
53+
containerCount := len(net.Containers)
54+
55+
networkData := models.DockerNetwork{
56+
NetworkID: net.ID,
57+
Name: net.Name,
58+
Driver: net.Driver,
59+
Scope: net.Scope,
60+
IPv6Enabled: net.EnableIPv6,
61+
Internal: net.Internal,
62+
Attachable: net.Attachable,
63+
Ingress: net.Ingress,
64+
ConfigOnly: net.ConfigOnly,
65+
Labels: net.Labels,
66+
IPAM: ipam,
67+
ContainerCount: containerCount,
68+
}
69+
70+
// Note: Docker networks don't have a CreatedAt timestamp in the List response
71+
// We'd need to inspect each network individually to get it, which is expensive
72+
// So we'll leave CreatedAt as nil for now
73+
74+
result = append(result, networkData)
75+
}
76+
77+
return result, nil
78+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"patchmon-agent/pkg/models"
9+
10+
"github.com/docker/docker/api/types"
11+
"github.com/docker/docker/api/types/volume"
12+
)
13+
14+
// collectVolumes collects all Docker volumes
15+
func (d *Integration) collectVolumes(ctx context.Context) ([]models.DockerVolume, error) {
16+
// List all volumes
17+
volumes, err := d.client.VolumeList(ctx, volume.ListOptions{})
18+
if err != nil {
19+
return nil, fmt.Errorf("failed to list volumes: %w", err)
20+
}
21+
22+
result := make([]models.DockerVolume, 0, len(volumes.Volumes))
23+
24+
// Get system disk usage info to include volume sizes
25+
diskUsage, err := d.client.DiskUsage(ctx, types.DiskUsageOptions{})
26+
if err != nil {
27+
d.logger.WithError(err).Debug("Failed to get disk usage (volume sizes unavailable)")
28+
}
29+
30+
// Create a map of volume name to usage for quick lookup
31+
volumeUsage := make(map[string]int64)
32+
if err == nil {
33+
for _, vol := range diskUsage.Volumes {
34+
if vol.UsageData != nil {
35+
volumeUsage[vol.Name] = vol.UsageData.Size
36+
}
37+
}
38+
}
39+
40+
for _, vol := range volumes.Volumes {
41+
// Parse created timestamp
42+
var createdAt *time.Time
43+
if vol.CreatedAt != "" {
44+
// Docker returns RFC3339Nano format
45+
if t, err := time.Parse(time.RFC3339Nano, vol.CreatedAt); err == nil {
46+
createdAt = &t
47+
}
48+
}
49+
50+
// Get volume size if available
51+
var sizeBytes *int64
52+
if size, found := volumeUsage[vol.Name]; found {
53+
sizeBytes = &size
54+
}
55+
56+
// Get driver renderer if available (e.g., "overlay2" for overlay2 driver)
57+
renderer := ""
58+
if vol.Options != nil {
59+
if t, ok := vol.Options["type"]; ok {
60+
renderer = t
61+
}
62+
}
63+
64+
volumeData := models.DockerVolume{
65+
VolumeID: vol.Name, // Docker volumes are identified by name
66+
Name: vol.Name,
67+
Driver: vol.Driver,
68+
Mountpoint: vol.Mountpoint,
69+
Renderer: renderer,
70+
Scope: vol.Scope,
71+
Labels: vol.Labels,
72+
Options: vol.Options,
73+
CreatedAt: createdAt,
74+
SizeBytes: sizeBytes,
75+
RefCount: 0, // Will be set below if available
76+
}
77+
78+
// Get reference count from usage data if available
79+
if vol.UsageData != nil && vol.UsageData.RefCount >= 0 {
80+
volumeData.RefCount = int(vol.UsageData.RefCount)
81+
}
82+
83+
result = append(result, volumeData)
84+
}
85+
86+
return result, nil
87+
}

internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package version
22

33
// Version represents the current version of the patchmon-agent
4-
const Version = "1.3.1"
4+
const Version = "1.3.2"

0 commit comments

Comments
 (0)