Files
pulse-triage[bot] 696cb39c08 Bound container stats response bodies
Change-source: pulse-maintainer
2026-09-01 08:47:51 +01:00

805 lines
26 KiB
Go

package dockeragent
import (
"context"
"errors"
"io"
"net/http"
"net/netip"
"os"
"path/filepath"
"strings"
"testing"
"time"
containertypes "github.com/moby/moby/api/types/container"
imagetypes "github.com/moby/moby/api/types/image"
networktypes "github.com/moby/moby/api/types/network"
swarmtypes "github.com/moby/moby/api/types/swarm"
systemtypes "github.com/moby/moby/api/types/system"
volumetypes "github.com/moby/moby/api/types/volume"
dockerclient "github.com/moby/moby/client"
"github.com/rs/zerolog"
)
func TestBuildHostSecurityInfoAuthorizationPlugins(t *testing.T) {
security := buildHostSecurityInfo(systemtypes.Info{
Plugins: systemtypes.PluginsInfo{
Authorization: []string{"opa", " audit ", "opa", ""},
},
})
if security == nil {
t.Fatalf("expected host security info")
}
if got := security.AuthorizationPlugins; len(got) != 2 || got[0] != "opa" || got[1] != "audit" {
t.Fatalf("expected normalized authorization plugins, got %#v", got)
}
}
func TestCollectStorageUsageDecouplesFullDaemonScanFromLiveTelemetry(t *testing.T) {
calls := 0
agent := &Agent{
logger: zerolog.Nop(),
docker: &fakeDockerClient{
diskUsageFn: func(context.Context, dockerDiskUsageOptions) (dockerclient.DiskUsageResult, error) {
calls++
if calls > 1 {
return dockerclient.DiskUsageResult{}, context.DeadlineExceeded
}
return dockerclient.DiskUsageResult{
Containers: dockerclient.ContainersDiskUsage{TotalCount: 47, ActiveCount: 4, TotalSize: 4096},
}, nil
},
},
}
firstResult, firstUsage, err := agent.collectStorageUsage(context.Background())
if err != nil || firstUsage == nil || firstUsage.Containers.TotalCount != 47 {
t.Fatalf("first storage scan = %#v, %#v, %v", firstResult, firstUsage, err)
}
secondResult, secondUsage, err := agent.collectStorageUsage(context.Background())
if err != nil || secondUsage == nil || secondUsage.Containers.TotalCount != 47 || calls != 1 {
t.Fatalf("cached storage scan = %#v, %#v, %v; calls=%d, want one daemon scan", secondResult, secondUsage, err, calls)
}
if firstUsage == secondUsage {
t.Fatal("cached storage usage must be returned as an independent value")
}
agent.storageUsageMu.Lock()
agent.storageUsageCache.nextRefresh = time.Now().Add(-time.Second)
agent.storageUsageMu.Unlock()
staleResult, staleUsage, err := agent.collectStorageUsage(context.Background())
if !errors.Is(err, context.DeadlineExceeded) || staleUsage == nil || staleUsage.Containers.TotalCount != 47 || calls != 2 {
t.Fatalf("failed refresh = %#v, %#v, %v; calls=%d, want one attempt plus retained cache", staleResult, staleUsage, err, calls)
}
_, retainedUsage, err := agent.collectStorageUsage(context.Background())
if err != nil || retainedUsage == nil || retainedUsage.Containers.TotalCount != 47 || calls != 2 {
t.Fatalf("post-failure cache = %#v, %v; calls=%d, want throttled retained result", retainedUsage, err, calls)
}
}
func TestCollectStorageUsageThrottlesInitialTransientFailureWithoutRetry(t *testing.T) {
calls := 0
agent := &Agent{
logger: zerolog.Nop(),
docker: &fakeDockerClient{
diskUsageFn: func(context.Context, dockerDiskUsageOptions) (dockerclient.DiskUsageResult, error) {
calls++
return dockerclient.DiskUsageResult{}, context.DeadlineExceeded
},
},
}
if _, _, err := agent.collectStorageUsage(context.Background()); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("initial scan error = %v, want deadline", err)
}
if _, usage, err := agent.collectStorageUsage(context.Background()); err != nil || usage != nil {
t.Fatalf("throttled scan = %#v, %v; want no repeated error or data", usage, err)
}
if calls != 1 {
t.Fatalf("DiskUsage calls = %d, want one attempt across the refresh window", calls)
}
}
func TestExtractHealthcheckTargetsKeepsOnlyNormalizedURLHosts(t *testing.T) {
targets := extractHealthcheckTargets([]string{
"CMD-SHELL",
"wget -qO- 'http://Dependency-Service:8080/health?token=private' && curl https://user:password@api.internal.example/status",
"curl http://dependency-service:9090/duplicate",
"curl 'http://[2001:db8::1]:8080/ready'",
"printf 'not-a-url secret=value'",
})
want := []string{"dependency-service", "api.internal.example", "2001:db8::1"}
if len(targets) != len(want) {
t.Fatalf("health-check targets = %#v, want %#v", targets, want)
}
for i := range want {
if targets[i] != want[i] {
t.Fatalf("health-check target %d = %q, want %q", i, targets[i], want[i])
}
}
joined := strings.Join(targets, " ")
for _, forbidden := range []string{"token", "private", "user", "password", "/health", "/status"} {
if strings.Contains(joined, forbidden) {
t.Fatalf("health-check targets leaked %q: %q", forbidden, joined)
}
}
}
func TestCollectDockerNativeInventory(t *testing.T) {
createdAt := time.Date(2026, 5, 24, 8, 0, 0, 0, time.UTC)
agent := &Agent{
logger: zerolog.Nop(),
docker: &fakeDockerClient{
imageListFn: func(_ context.Context, opts dockerImageListOptions) ([]imagetypes.Summary, error) {
if opts.SharedSize {
t.Fatal("live image inventory must not request shared-size computation")
}
return []imagetypes.Summary{{
ID: " sha256:image1 ",
RepoTags: []string{"repo/app:latest", " "},
RepoDigests: []string{"repo/app@sha256:abc"},
Size: 1024,
SharedSize: -1,
Containers: -1,
Created: createdAt.Unix(),
Labels: map[string]string{"tier": "web"},
}}, nil
},
volumeListFn: func(context.Context, dockerVolumeListOptions) ([]volumetypes.Volume, error) {
return []volumetypes.Volume{{
Name: " app-data ",
Driver: " local ",
Mountpoint: "/var/lib/docker/volumes/app-data",
Scope: " local ",
Labels: map[string]string{"backup": "true"},
}}, nil
},
networkListFn: func(context.Context, dockerNetworkListOptions) ([]networktypes.Summary, error) {
return []networktypes.Summary{{
Network: networktypes.Network{
ID: " net1 ",
Name: " app-net ",
Driver: " bridge ",
Scope: " local ",
EnableIPv4: true,
Attachable: true,
IPAM: networktypes.IPAM{Config: []networktypes.IPAMConfig{{
Subnet: netip.MustParsePrefix("10.88.0.0/24"),
Gateway: netip.MustParseAddr("10.88.0.1"),
}}},
Labels: map[string]string{"env": "prod"},
Options: map[string]string{"mtu": "1500"},
},
}}, nil
},
diskUsageFn: func(context.Context, dockerDiskUsageOptions) (dockerclient.DiskUsageResult, error) {
return dockerclient.DiskUsageResult{
Images: dockerclient.ImagesDiskUsage{
TotalCount: 3, ActiveCount: 2, TotalSize: 4096, Reclaimable: 512,
Items: []imagetypes.Summary{{ID: "sha256:image1", SharedSize: 256, Containers: 2}},
},
Volumes: dockerclient.VolumesDiskUsage{
TotalCount: 1,
Items: []volumetypes.Volume{{
Name: "app-data",
UsageData: &volumetypes.UsageData{Size: 2048, RefCount: 4},
}},
},
}, nil
},
},
}
usageResult, storageUsage, err := agent.collectStorageUsage(context.Background())
if err != nil {
t.Fatalf("collectStorageUsage: %v", err)
}
if storageUsage.Images.TotalCount != 3 || storageUsage.Images.ReclaimableBytes != 512 {
t.Fatalf("unexpected storage usage: %+v", storageUsage)
}
images, err := agent.collectImages(context.Background(), usageResult.Images.Items)
if err != nil {
t.Fatalf("collectImages: %v", err)
}
if len(images) != 1 || images[0].ID != "sha256:image1" || images[0].CreatedAt != createdAt {
t.Fatalf("unexpected images: %+v", images)
}
if len(images[0].RepoTags) != 1 || images[0].RepoTags[0] != "repo/app:latest" {
t.Fatalf("expected normalized repo tags, got %#v", images[0].RepoTags)
}
if images[0].SharedSizeBytes != 256 || images[0].Containers != 2 {
t.Fatalf("image storage projection = %+v, want cached shared-size and container counts", images[0])
}
volumes, err := agent.collectVolumes(context.Background(), usageResult.Volumes.Items)
if err != nil {
t.Fatalf("collectVolumes: %v", err)
}
if len(volumes) != 1 || volumes[0].Name != "app-data" || volumes[0].SizeBytes != 2048 || volumes[0].RefCount != 4 {
t.Fatalf("unexpected volumes: %+v", volumes)
}
networks, err := agent.collectNetworks(context.Background())
if err != nil {
t.Fatalf("collectNetworks: %v", err)
}
if len(networks) != 1 || networks[0].Name != "app-net" || !networks[0].EnableIPv4 || !networks[0].Attachable {
t.Fatalf("unexpected networks: %+v", networks)
}
if len(networks[0].Subnets) != 1 || networks[0].Subnets[0].Subnet != "10.88.0.0/24" || networks[0].Subnets[0].Gateway != "10.88.0.1" {
t.Fatalf("unexpected network subnets: %+v", networks[0].Subnets)
}
}
func TestCollectSwarmNodesMapsManagerInventory(t *testing.T) {
createdAt := time.Date(2026, 5, 24, 8, 15, 0, 0, time.UTC)
updatedAt := createdAt.Add(2 * time.Minute)
agent := &Agent{
logger: zerolog.Nop(),
docker: &fakeDockerClient{
nodeListFn: func(context.Context, dockerNodeListOptions) ([]swarmtypes.Node, error) {
return []swarmtypes.Node{{
ID: " node-1 ",
Spec: swarmtypes.NodeSpec{
Role: swarmtypes.NodeRoleManager,
Availability: swarmtypes.NodeAvailabilityActive,
Annotations: swarmtypes.Annotations{
Labels: map[string]string{"zone": "rack-a"},
},
},
Description: swarmtypes.NodeDescription{
Hostname: " manager-1 ",
Platform: swarmtypes.Platform{
OS: "linux",
Architecture: "amd64",
},
Resources: swarmtypes.Resources{
NanoCPUs: 8_000_000_000,
MemoryBytes: 32 * 1024 * 1024 * 1024,
},
Engine: swarmtypes.EngineDescription{
EngineVersion: "27.5.1",
Labels: map[string]string{"engine": "primary"},
},
},
Status: swarmtypes.NodeStatus{
State: swarmtypes.NodeStateReady,
Message: "ready",
Addr: "192.0.2.10",
},
ManagerStatus: &swarmtypes.ManagerStatus{
Leader: true,
Reachability: swarmtypes.ReachabilityReachable,
Addr: "192.0.2.10:2377",
},
Meta: swarmtypes.Meta{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
}}, nil
},
},
}
nodes, err := agent.collectSwarmNodes(context.Background())
if err != nil {
t.Fatalf("collectSwarmNodes: %v", err)
}
if len(nodes) != 1 {
t.Fatalf("expected one node, got %+v", nodes)
}
node := nodes[0]
if node.ID != "node-1" || node.Hostname != "manager-1" || node.Role != string(swarmtypes.NodeRoleManager) {
t.Fatalf("unexpected node identity: %+v", node)
}
if node.ManagerReachability != string(swarmtypes.ReachabilityReachable) || node.ManagerAddress != "192.0.2.10:2377" || !node.Leader {
t.Fatalf("expected manager status to be preserved, got %+v", node)
}
if node.EngineVersion != "27.5.1" || node.NanoCPUs != 8_000_000_000 || node.MemoryBytes == 0 {
t.Fatalf("expected engine capacity to be preserved, got %+v", node)
}
if node.Labels["zone"] != "rack-a" || node.EngineLabels["engine"] != "primary" {
t.Fatalf("expected node labels to be preserved, got labels=%+v engine=%+v", node.Labels, node.EngineLabels)
}
if !node.CreatedAt.Equal(createdAt) || node.UpdatedAt == nil || !node.UpdatedAt.Equal(updatedAt) {
t.Fatalf("expected timestamps to be preserved, got %+v", node)
}
}
func TestCollectContainer(t *testing.T) {
logger := zerolog.Nop()
t.Run("success with running container", func(t *testing.T) {
stats := containertypes.StatsResponse{
Read: time.Now(),
CPUStats: containertypes.CPUStats{
CPUUsage: containertypes.CPUUsage{TotalUsage: 200000000},
SystemUsage: 2000000000,
OnlineCPUs: 2,
},
PreCPUStats: containertypes.CPUStats{
CPUUsage: containertypes.CPUUsage{TotalUsage: 100000000},
SystemUsage: 1000000000,
},
MemoryStats: containertypes.MemoryStats{
Usage: 1000000,
Limit: 4000000,
Stats: map[string]uint64{"cache": 200000},
},
Networks: map[string]containertypes.NetworkStats{
"eth0": {RxBytes: 2048, TxBytes: 1024},
"eth1": {RxBytes: 512, TxBytes: 256},
},
BlkioStats: containertypes.BlkioStats{
IoServiceBytesRecursive: []containertypes.BlkioStatEntry{
{Op: "Read", Value: 100},
{Op: "Write", Value: 200},
},
},
}
sizeRw := int64(1234)
sizeRoot := int64(5678)
inspect := baseInspect()
inspect.SizeRw = &sizeRw
inspect.SizeRootFs = &sizeRoot
inspect.State = &containertypes.State{
Running: true,
StartedAt: time.Now().Add(-time.Minute).Format(time.RFC3339Nano),
Health: &containertypes.Health{Status: "healthy"},
}
inspect.Config.Env = []string{"PASSWORD=secret", "PATH=/bin"}
inspect.Config.Image = "nginx@sha256:abc123"
inspect.NetworkSettings.Networks["net1"].IPAddress = netip.MustParseAddr("10.0.0.2")
inspect.Mounts = []containertypes.MountPoint{
{Type: "bind", Source: "/data", Destination: "/data", RW: true},
}
agent := &Agent{
cfg: Config{
CollectDiskMetrics: true,
},
runtime: RuntimePodman,
logger: logger,
prevContainerCPU: make(map[string]cpuSample),
registryChecker: NewRegistryChecker(logger),
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
return statsReader(t, stats), nil
},
},
}
summary := containertypes.Summary{
ID: "container-123456",
Names: []string{"/app"},
Image: "nginx@sha256:abc123",
ImageID: "sha256:local",
Created: time.Now().Add(-time.Hour).Unix(),
State: "running",
Status: "Up",
Ports: []containertypes.PortSummary{
{PrivatePort: 80, PublicPort: 8080, Type: "tcp", IP: netip.MustParseAddr("0.0.0.0")},
},
Labels: map[string]string{
"io.podman.annotations.pod.name": "mypod",
},
}
container, err := agent.collectContainer(context.Background(), summary)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if container.Name != "app" {
t.Fatalf("expected name app, got %q", container.Name)
}
if container.Health != "healthy" {
t.Fatalf("expected health status, got %q", container.Health)
}
if container.OOMKilled == nil || *container.OOMKilled {
t.Fatalf("expected authoritative non-OOM state, got %v", container.OOMKilled)
}
if container.BlockIO == nil {
t.Fatalf("expected block IO to be populated")
}
if container.Podman == nil || container.Podman.PodName != "mypod" {
t.Fatalf("expected podman metadata")
}
if container.UpdateStatus == nil {
t.Fatal("expected update status for digest-pinned image, got nil")
}
if container.UpdateStatus.Error == "" {
t.Fatalf("expected update status for digest-pinned image, got empty error. Status: %+v", container.UpdateStatus)
}
if len(container.Networks) == 0 {
t.Fatalf("expected networks to be populated")
}
if container.NetworkRXBytes != 2560 || container.NetworkTXBytes != 1280 {
t.Fatalf("unexpected network totals: rx=%d tx=%d", container.NetworkRXBytes, container.NetworkTXBytes)
}
})
t.Run("podman uses reported cpu percent from compat stats payload", func(t *testing.T) {
inspect := baseInspect()
inspect.State = &containertypes.State{
Running: true,
StartedAt: time.Now().Add(-time.Minute).Format(time.RFC3339Nano),
}
agent := &Agent{
runtime: RuntimePodman,
logger: logger,
prevContainerCPU: map[string]cpuSample{
"container-123456": {
totalUsage: 100_000_000,
systemUsage: 1_000_000_000,
onlineCPUs: 16,
read: time.Now().Add(-time.Second),
},
},
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
payload := `{
"read":"2026-04-09T12:00:00Z",
"cpu_stats":{
"cpu_usage":{"total_usage":130000000},
"system_cpu_usage":1010000000,
"online_cpus":16,
"cpu":0.32,
"throttling_data":{}
},
"precpu_stats":{},
"memory_stats":{"usage":1000000,"limit":4000000,"stats":{"cache":200000}},
"blkio_stats":{"io_service_bytes_recursive":[]}
}`
return dockerStatsResponseReader{Body: io.NopCloser(strings.NewReader(payload))}, nil
},
},
}
container, err := agent.collectContainer(context.Background(), containertypes.Summary{
ID: "container-123456",
Names: []string{"/app"},
Image: "nginx:latest",
ImageID: "sha256:local",
Created: time.Now().Add(-time.Hour).Unix(),
State: "running",
Status: "Up",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if container.CPUPercent != 0.32 {
t.Fatalf("expected podman cpu percent 0.32 from payload, got %f", container.CPUPercent)
}
})
t.Run("stopped container clears sample", func(t *testing.T) {
agent := &Agent{
logger: logger,
prevContainerCPU: map[string]cpuSample{
"container-123456": {totalUsage: 1},
},
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
inspect := baseInspect()
inspect.State = &containertypes.State{Running: false}
return inspect, nil, nil
},
},
}
summary := containertypes.Summary{ID: "container-123456", Names: []string{"/app"}, State: "exited"}
if _, err := agent.collectContainer(context.Background(), summary); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := agent.prevContainerCPU["container1"]; ok {
t.Fatalf("expected sample to be removed")
}
})
t.Run("inspect error", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
return containertypes.InspectResponse{}, nil, errors.New("inspect failed")
},
},
logger: logger,
}
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
t.Fatal("expected error")
}
})
t.Run("stats error", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
inspect := baseInspect()
inspect.State = &containertypes.State{Running: true}
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
return dockerStatsResponseReader{}, errors.New("stats failed")
},
},
logger: logger,
}
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
t.Fatal("expected error")
}
})
t.Run("stats decode error", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
inspect := baseInspect()
inspect.State = &containertypes.State{Running: true}
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
return dockerStatsResponseReader{Body: io.NopCloser(strings.NewReader("{"))}, nil
},
},
logger: logger,
}
if _, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"}); err == nil {
t.Fatal("expected error")
}
})
t.Run("oversized stats response", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
inspect := baseInspect()
inspect.State = &containertypes.State{Running: true}
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
return dockerStatsResponseReader{
Body: io.NopCloser(strings.NewReader(strings.Repeat("x", maxContainerStatsBodyBytes+1))),
}, nil
},
},
logger: logger,
}
_, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456"})
if err == nil || !strings.Contains(err.Error(), "response body exceeds") {
t.Fatalf("collectContainer() error = %v, want response size limit error", err)
}
})
t.Run("uptime negative clamped", func(t *testing.T) {
future := time.Now().Add(5 * time.Minute).Format(time.RFC3339Nano)
inspect := baseInspect()
inspect.State = &containertypes.State{
Running: true,
StartedAt: future,
}
stats := containertypes.StatsResponse{
Read: time.Now(),
CPUStats: containertypes.CPUStats{
CPUUsage: containertypes.CPUUsage{TotalUsage: 100},
SystemUsage: 1000,
OnlineCPUs: 1,
},
PreCPUStats: containertypes.CPUStats{
CPUUsage: containertypes.CPUUsage{TotalUsage: 100},
SystemUsage: 1000,
},
}
agent := &Agent{
logger: logger,
prevContainerCPU: make(map[string]cpuSample),
docker: &fakeDockerClient{
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
return inspect, nil, nil
},
containerStatsOneShotFn: func(context.Context, string) (dockerStatsResponseReader, error) {
return statsReader(t, stats), nil
},
},
}
container, err := agent.collectContainer(context.Background(), containertypes.Summary{ID: "container-123456", Names: []string{"/app"}, State: "running"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if container.UptimeSeconds != 0 {
t.Fatalf("expected uptime to be clamped to 0, got %d", container.UptimeSeconds)
}
})
}
func TestCollectContainers(t *testing.T) {
logger := zerolog.Nop()
t.Run("list error", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
containerListFunc: func(context.Context, dockerContainerListOptions) ([]containertypes.Summary, error) {
return nil, errors.New("list failed")
},
},
logger: logger,
}
if _, err := agent.collectContainers(context.Background()); err == nil {
t.Fatal("expected error")
}
})
t.Run("filters and prune", func(t *testing.T) {
agent := &Agent{
logger: logger,
stateFilters: []string{"running"},
allowedStates: map[string]struct{}{
"running": {},
},
prevContainerCPU: map[string]cpuSample{
"stale": {totalUsage: 1},
},
docker: &fakeDockerClient{
containerListFunc: func(_ context.Context, opts dockerContainerListOptions) ([]containertypes.Summary, error) {
if opts.Filters.Len() == 0 {
t.Fatal("expected filters to be set")
}
return []containertypes.Summary{
{ID: "running1", Names: []string{"/run"}, State: "running"},
{ID: "exited1", Names: []string{"/exit"}, State: "exited"},
}, nil
},
containerInspectWithRawFn: func(context.Context, string, bool) (containertypes.InspectResponse, []byte, error) {
return containertypes.InspectResponse{}, nil, errors.New("inspect failed")
},
},
}
containers, err := agent.collectContainers(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(containers) != 0 {
t.Fatalf("expected no containers, got %d", len(containers))
}
if _, ok := agent.prevContainerCPU["stale"]; ok {
t.Fatalf("expected stale sample to be pruned")
}
})
}
func TestPrimaryTargetAndHTTPClient(t *testing.T) {
t.Run("primary target empty", func(t *testing.T) {
agent := &Agent{}
if target := agent.primaryTarget(); target.URL != "" {
t.Fatal("expected empty target")
}
})
t.Run("http client selection", func(t *testing.T) {
secure := &http.Client{}
insecure := &http.Client{}
agent := &Agent{
httpClients: map[bool]*http.Client{
false: secure,
true: insecure,
},
}
if got := agent.httpClientFor(TargetConfig{InsecureSkipVerify: true}); got != insecure {
t.Fatal("expected insecure client")
}
if got := agent.httpClientFor(TargetConfig{InsecureSkipVerify: false}); got != secure {
t.Fatal("expected secure client")
}
})
t.Run("custom trust client selection", func(t *testing.T) {
custom := &http.Client{}
target := TargetConfig{CACertPath: "/etc/pulse/ca.pem", ServerFingerprint: "abc"}
agent := &Agent{
httpClients: map[bool]*http.Client{false: {}},
trustedHTTPClients: map[string]*http.Client{
targetTrustKey(target): custom,
},
}
if got := agent.httpClientFor(target); got != custom {
t.Fatal("expected target-specific trust client")
}
})
t.Run("http client fallback", func(t *testing.T) {
agent := &Agent{
httpClients: map[bool]*http.Client{},
}
got := agent.httpClientFor(TargetConfig{InsecureSkipVerify: true})
if got == nil {
t.Fatal("expected fallback client")
}
})
t.Run("http client fallback prefers secure", func(t *testing.T) {
secure := &http.Client{}
agent := &Agent{
httpClients: map[bool]*http.Client{
false: secure,
},
}
got := agent.httpClientFor(TargetConfig{InsecureSkipVerify: true})
if got != secure {
t.Fatal("expected secure fallback client")
}
})
t.Run("http client fallback prefers insecure", func(t *testing.T) {
insecure := &http.Client{}
agent := &Agent{
httpClients: map[bool]*http.Client{
true: insecure,
},
}
got := agent.httpClientFor(TargetConfig{InsecureSkipVerify: false})
if got != insecure {
t.Fatal("expected insecure fallback client")
}
})
}
func TestNewHTTPClient(t *testing.T) {
client := newHTTPClient(true)
if client.Transport == nil {
t.Fatal("expected transport")
}
transport := client.Transport.(*http.Transport)
if !transport.TLSClientConfig.InsecureSkipVerify {
t.Fatal("expected insecure skip verify true")
}
}
func TestNewHTTPClientWithTrustRejectsInvalidCABundle(t *testing.T) {
path := filepath.Join(t.TempDir(), "invalid-ca.pem")
if err := os.WriteFile(path, []byte("not a certificate"), 0o600); err != nil {
t.Fatalf("write invalid CA: %v", err)
}
if _, err := newHTTPClientWithTrust(path, false, ""); err == nil {
t.Fatal("expected invalid custom CA bundle to fail")
}
}
func TestAgentClose(t *testing.T) {
closed := false
agent := &Agent{
docker: &fakeDockerClient{
closeFn: func() error {
closed = true
return nil
},
},
}
if err := agent.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !closed {
t.Fatal("expected docker Close to be called")
}
}