diff --git a/daemon/stats.go b/daemon/stats.go index 5d6550e420..f5b0e06ef6 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -11,6 +11,7 @@ import ( containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/v2/daemon/container" "github.com/moby/moby/v2/daemon/server/backend" + "github.com/pkg/errors" ) // ContainerStats writes information about the container to the stream @@ -124,8 +125,10 @@ func (daemon *Daemon) GetContainerStats(ctr *container.Container) (*containertyp goto done } - // We already have the network stats on Windows directly from HCS. - if !ctr.Config.NetworkDisabled && runtime.GOOS != "windows" { + // On Windows, the builtin (HCS) runtime already provides network stats + // embedded in the stats above; the containerd runtime does not, so query + // them separately when they're missing. + if !ctr.Config.NetworkDisabled && (runtime.GOOS != "windows" || len(stats.Networks) == 0) { stats.Networks, err = daemon.getNetworkStats(ctr) } @@ -144,3 +147,18 @@ done: } return stats, nil } + +// getNetworkSandboxID resolves the network SandboxID, following the chain in +// case the container reuses another container's network stack. +func (daemon *Daemon) getNetworkSandboxID(c *container.Container) (string, error) { + curr := c + for curr.HostConfig.NetworkMode.IsContainer() { + containerID := curr.HostConfig.NetworkMode.ConnectedContainer() + connected, err := daemon.GetContainer(containerID) + if err != nil { + return "", errors.Wrapf(err, "Could not get container for %s", containerID) + } + curr = connected + } + return curr.NetworkSettings.SandboxID, nil +} diff --git a/daemon/stats_unix.go b/daemon/stats_unix.go index 75e9e2b31a..b56a2f32ee 100644 --- a/daemon/stats_unix.go +++ b/daemon/stats_unix.go @@ -259,20 +259,6 @@ func (daemon *Daemon) statsV2(s *containertypes.StatsResponse, stats *statsV2.Me return s, nil } -// Resolve Network SandboxID in case the container reuse another container's network stack -func (daemon *Daemon) getNetworkSandboxID(c *container.Container) (string, error) { - curr := c - for curr.HostConfig.NetworkMode.IsContainer() { - containerID := curr.HostConfig.NetworkMode.ConnectedContainer() - connected, err := daemon.GetContainer(containerID) - if err != nil { - return "", errors.Wrapf(err, "Could not get container for %s", containerID) - } - curr = connected - } - return curr.NetworkSettings.SandboxID, nil -} - func (daemon *Daemon) getNetworkStats(c *container.Container) (map[string]containertypes.NetworkStats, error) { sandboxID, err := daemon.getNetworkSandboxID(c) if err != nil { diff --git a/daemon/stats_windows.go b/daemon/stats_windows.go index e2c6ec0043..73ae94234c 100644 --- a/daemon/stats_windows.go +++ b/daemon/stats_windows.go @@ -4,6 +4,7 @@ import ( "context" "runtime" + "github.com/Microsoft/hcsshim" cerrdefs "github.com/containerd/errdefs" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/v2/daemon/container" @@ -78,9 +79,55 @@ func (daemon *Daemon) stats(c *container.Container) (*containertypes.StatsRespon return s, nil } -// Windows network stats are obtained directly through HCS, hence this is a no-op. +// getNetworkStats collects network statistics for a container. The builtin HCS +// runtime already embeds these in the container stats, but the containerd +// runtime does not, so they are queried here directly from HNS by endpoint. func (daemon *Daemon) getNetworkStats(c *container.Container) (map[string]containertypes.NetworkStats, error) { - return make(map[string]containertypes.NetworkStats), nil + sandboxID, err := daemon.getNetworkSandboxID(c) + if err != nil { + return nil, err + } + + sb, err := daemon.netController.SandboxByID(sandboxID) + if err != nil { + return nil, err + } + + stats := make(map[string]containertypes.NetworkStats) + for _, ep := range sb.Endpoints() { + info, err := ep.DriverInfo() + if err != nil { + return nil, err + } + hnsid, ok := info["hnsid"].(string) + if !ok || hnsid == "" { + continue + } + + epStats, err := hcsshim.GetHNSEndpointStats(hnsid) + if err != nil { + return nil, err + } + + stats[epStats.EndpointID] = hnsStatsToNetworkStats(epStats) + } + return stats, nil +} + +// hnsStatsToNetworkStats maps HNS endpoint statistics onto the API network stats +// shape. Errors are not reported by HNS, so RxErrors/TxErrors are left zeroed +// (matching the builtin HCS path, which also does not populate them on Windows). +func hnsStatsToNetworkStats(epStats *hcsshim.HNSEndpointStats) containertypes.NetworkStats { + return containertypes.NetworkStats{ + RxBytes: epStats.BytesReceived, + RxPackets: epStats.PacketsReceived, + RxDropped: epStats.DroppedPacketsIncoming, + TxBytes: epStats.BytesSent, + TxPackets: epStats.PacketsSent, + TxDropped: epStats.DroppedPacketsOutgoing, + EndpointID: epStats.EndpointID, + InstanceID: epStats.InstanceID, + } } // getSystemCPUUsage returns the host system's cpu usage in diff --git a/daemon/stats_windows_test.go b/daemon/stats_windows_test.go new file mode 100644 index 0000000000..0f4b6dd4b0 --- /dev/null +++ b/daemon/stats_windows_test.go @@ -0,0 +1,37 @@ +package daemon + +import ( + "testing" + + "github.com/Microsoft/hcsshim" + containertypes "github.com/moby/moby/api/types/container" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestHNSStatsToNetworkStats(t *testing.T) { + in := &hcsshim.HNSEndpointStats{ + EndpointID: "endpoint-1", + InstanceID: "instance-1", + BytesReceived: 100, + PacketsReceived: 10, + DroppedPacketsIncoming: 1, + BytesSent: 200, + PacketsSent: 20, + DroppedPacketsOutgoing: 2, + } + + got := hnsStatsToNetworkStats(in) + + assert.Check(t, is.DeepEqual(got, containertypes.NetworkStats{ + RxBytes: 100, + RxPackets: 10, + RxDropped: 1, + TxBytes: 200, + TxPackets: 20, + TxDropped: 2, + EndpointID: "endpoint-1", + InstanceID: "instance-1", + // RxErrors/TxErrors are not reported by HNS and remain zero. + })) +} diff --git a/integration/container/stats_test.go b/integration/container/stats_test.go index bb0c006ebf..5d09385c9e 100644 --- a/integration/container/stats_test.go +++ b/integration/container/stats_test.go @@ -101,8 +101,6 @@ func TestStatsContainerNotFound(t *testing.T) { } func TestStatsNetworkStats(t *testing.T) { - // FIXME(thaJeztah): Broken on Windows + containerd combination, see https://github.com/moby/moby/pull/41479 - skip.If(t, testEnv.RuntimeIsWindowsContainerd(), "FIXME: Broken on Windows + containerd combination") skip.If(t, testEnv.IsRootless() && testEnv.DaemonInfo.CgroupVersion == "1", "Rootless Mode does not support cgroups v1 stats") skip.If(t, testEnv.IsRemoteDaemon(), "Test requires a local daemon")