Skip to content

Commit

Permalink
Merge pull request #266 from RazCrimson/main
Browse files Browse the repository at this point in the history
Streaming support for PodsManager.stats API
  • Loading branch information
rhatdan authored May 31, 2023
2 parents b02001a + 7e5665d commit 6cf648d
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 5 deletions.
20 changes: 16 additions & 4 deletions podman/domain/pods_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""PodmanResource manager subclassed for Networks."""
import json
import logging
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union, Iterator

from podman import api
from podman.domain.manager import Manager
Expand Down Expand Up @@ -128,12 +128,14 @@ def remove(self, pod_id: Union[Pod, str], force: Optional[bool] = None) -> None:
response = self.client.delete(f"/pods/{pod_id}", params={"force": force})
response.raise_for_status()

def stats(self, **kwargs) -> Dict[str, Any]:
def stats(self, **kwargs) -> Union[List[Dict[str, Any]], Iterator[List[Dict[str, Any]]]]:
"""Resource usage statistics for the containers in pods.
Keyword Args:
all (bool): Provide statistics for all running pods.
name (Union[str, List[str]]): Pods to include in report.
stream (bool): Stream statistics until cancelled. Default: False.
decode (bool): If True, response will be decoded into dict. Default: False.
Raises:
NotFound: when pod not found
Expand All @@ -142,10 +144,20 @@ def stats(self, **kwargs) -> Dict[str, Any]:
if "all" in kwargs and "name" in kwargs:
raise ValueError("Keywords 'all' and 'name' are mutually exclusive.")

# Keeping the default for stream as False to not break existing users
# Should probably be changed in a newer major version to match behavior of container.stats
stream = kwargs.get("stream", False)
decode = kwargs.get("decode", False)

params = {
"all": kwargs.get("all"),
"namesOrIDs": kwargs.get("name"),
"stream": stream,
}
response = self.client.get("/pods/stats", params=params)
response = self.client.get("/pods/stats", params=params, stream=stream)
response.raise_for_status()
return response.json()

if stream:
return api.stream_helper(response, decode_to_json=decode)

return json.loads(response.content) if decode else response.content
88 changes: 87 additions & 1 deletion podman/tests/unit/test_podsmanager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import io
import json
import unittest
from typing import Iterable

import requests_mock

Expand Down Expand Up @@ -153,10 +156,93 @@ def test_stats(self, mock):
)

actual = self.client.pods.stats(
name="c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8"
name="c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8",
)
self.assertEqual(actual, json.dumps(body).encode())

@requests_mock.Mocker()
def test_stats_without_decode(self, mock):
body = {
"Processes": [
[
'jhonce',
'2417',
'2274',
'0',
'Mar01',
'?',
'00:00:01',
'/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"',
],
['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'],
['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'],
],
"Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"],
}
mock.get(
tests.LIBPOD_URL
+ "/pods/stats"
"?namesOrIDs=c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8",
json=body,
)

actual = self.client.pods.stats(
name="c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", decode=True
)
self.assertDictEqual(actual, body)

@requests_mock.Mocker()
def test_top_with_streaming(self, mock):
stream = [
[
{
'CPU': '2.53%',
'MemUsage': '49.15kB / 16.71GB',
'MemUsageBytes': '48KiB / 15.57GiB',
'Mem': '0.00%',
'NetIO': '7.638kB / 430B',
'BlockIO': '-- / --',
'PIDS': '1',
'Pod': '1c948ab42339',
'CID': 'd999c49a7b6c',
'Name': '1c948ab42339-infra',
}
],
[
{
'CPU': '1.46%',
'MemUsage': '57.23B / 16.71GB',
'MemUsageBytes': '48KiB / 15.57GiB',
'Mem': '0.00%',
'NetIO': '7.638kB / 430B',
'BlockIO': '-- / --',
'PIDS': '1',
'Pod': '1c948ab42339',
'CID': 'd999c49a7b6c',
'Name': '1c948ab42339-infra',
}
],
]

buffer = io.StringIO()
for entry in stream:
buffer.write(json.JSONEncoder().encode(entry))
buffer.write("\n")

adapter = mock.get(
tests.LIBPOD_URL + "/pods/stats?stream=True",
text=buffer.getvalue(),
)

stream_results = self.client.pods.stats(stream=True, decode=True)

self.assertIsInstance(stream_results, Iterable)
for response, actual in zip(stream_results, stream):
self.assertIsInstance(response, list)
self.assertListEqual(response, actual)

self.assertTrue(adapter.called_once)

def test_stats_400(self):
with self.assertRaises(ValueError):
self.client.pods.stats(all=True, name="container")
Expand Down

0 comments on commit 6cf648d

Please sign in to comment.