Extend API with update_container_from_resources

The update container command allows to update quite some properties.
From Docker's Swagger yaml it seems that all properties from the
Resource object are supported. Introduce a new API
update_container_from_resources similar to create_container_from_config
to support all properties supported by the Resources object.

Signed-off-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
Stefan Agner 2021-04-06 14:45:26 +02:00
parent c2b09f824c
commit 2d74a3bb1a
No known key found for this signature in database
GPG Key ID: AE01353D1E44747D
4 changed files with 194 additions and 37 deletions

View File

@ -8,6 +8,7 @@ from ..types import ContainerConfig
from ..types import EndpointConfig from ..types import EndpointConfig
from ..types import HostConfig from ..types import HostConfig
from ..types import NetworkingConfig from ..types import NetworkingConfig
from ..types import HostResources
class ContainerApiMixin: class ContainerApiMixin:
@ -607,6 +608,92 @@ class ContainerApiMixin:
kwargs['version'] = self._version kwargs['version'] = self._version
return HostConfig(*args, **kwargs) return HostConfig(*args, **kwargs)
def create_resources(self, *args, **kwargs):
"""
Create a dictionary for the ``resources`` argument to
:py:meth:`update_container`.
Args:
blkio_weight_device: Block IO weight (relative device weight) in
the form of: ``[{"Path": "device_path", "Weight": weight}]``.
blkio_weight: Block IO weight (relative weight), accepts a weight
value between 10 and 1000.
cgroup_parent (string): Path to `cgroups` under which the
container's `cgroup` is created. If the path is not absolute,
the path is considered to be relative to the `cgroups` path of
the init process. Cgroups are created if they do not already
exist.
cpu_period (int): The length of a CPU period in microseconds.
cpu_quota (int): Microseconds of CPU time that the container can
get in a CPU period.
cpu_rt_period (int): The length of a CPU real-time period in
microseconds. Set to 0 to allocate no time allocated to
real-time tasks.
cpu_rt_runtime (int): The length of a CPU real-time runtime in
microseconds. Set to 0 to allocate no time allocated to
real-time tasks.
cpu_shares (int): CPU shares (relative weight).
cpuset_cpus (str): CPUs in which to allow execution (``0-3``,
``0,1``).
cpuset_mems (str): Memory nodes (MEMs) in which to allow execution
(``0-3``, ``0,1``). Only effective on NUMA systems.
device_cgroup_rules (:py:class:`list`): A list of cgroup rules to
apply to the container.
device_read_bps: Limit read rate (bytes per second) from a device
in the form of: `[{"Path": "device_path", "Rate": rate}]`
device_read_iops: Limit read rate (IO per second) from a device.
device_write_bps: Limit write rate (bytes per second) from a
device.
device_write_iops: Limit write rate (IO per second) from a device.
devices (:py:class:`list`): Expose host devices to the container,
as a list of strings in the form
``<path_on_host>:<path_in_container>:<cgroup_permissions>``.
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
to have read-write access to the host's ``/dev/sda`` via a
node named ``/dev/xvda`` inside the container.
device_requests (:py:class:`list`): Expose host resources such as
GPUs to the container, as a list of
:py:class:`docker.types.DeviceRequest` instances.
kernel_memory (int or str): Kernel memory limit
mem_limit (float or str): Memory limit. Accepts float values
(which represent the memory limit of the created container in
bytes) or a string with a units identification char
(``100000b``, ``1000k``, ``128m``, ``1g``). If a string is
specified without a units character, bytes are assumed as an
mem_reservation (float or str): Memory soft limit.
mem_swappiness (int): Tune a container's memory swappiness
behavior. Accepts number between 0 and 100.
memswap_limit (str or int): Maximum amount of memory + swap a
container is allowed to consume.
nano_cpus (int): CPU quota in units of 10<sup>-9</sup> CPUs.
oom_kill_disable (bool): Whether to disable OOM killer.
pids_limit (int): Tune a container's pids limit. Set ``-1`` for
unlimited.
ulimits (:py:class:`list`): Ulimits to set inside the container,
as a list of :py:class:`docker.types.Ulimit` instances.
Returns:
(dict) A dictionary which can be passed to the ``resources``
argument to :py:meth:`update_container`.
Example:
>>> cli.create_resources(mem_limit='1g', cpuset_cpus='0-3')
{'MemLimit': '1g', 'CpusetCpus': '0-3'}
"""
if not kwargs:
kwargs = {}
if 'version' in kwargs:
raise TypeError(
"create_resources() got an unexpected "
"keyword argument 'version'"
)
kwargs['version'] = self._version
hres = HostResources(*args, **kwargs)
return hres
def create_networking_config(self, *args, **kwargs): def create_networking_config(self, *args, **kwargs):
""" """
Create a networking config dictionary to be used as the Create a networking config dictionary to be used as the
@ -1246,28 +1333,25 @@ class ContainerApiMixin:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
""" """
url = self._url('/containers/{0}/update', container) resources = self.create_resources(
data = {} blkio_weight=blkio_weight, cpu_period=cpu_period,
if blkio_weight: cpu_shares=cpu_shares, cpu_quota=cpu_quota,
data['BlkioWeight'] = blkio_weight cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
if cpu_period: mem_limit=mem_limit, mem_reservation=mem_reservation,
data['CpuPeriod'] = cpu_period memswap_limit=memswap_limit, kernel_memory=kernel_memory
if cpu_shares: )
data['CpuShares'] = cpu_shares
if cpu_quota: return self.update_container_from_resources(
data['CpuQuota'] = cpu_quota container, resources, restart_policy
if cpuset_cpus: )
data['CpusetCpus'] = cpuset_cpus
if cpuset_mems: @utils.minimum_version('1.22')
data['CpusetMems'] = cpuset_mems @utils.check_resource('container')
if mem_limit: def update_container_from_resources(self, container, resources,
data['Memory'] = utils.parse_bytes(mem_limit) restart_policy):
if mem_reservation: u = self._url('/containers/{0}/update', container)
data['MemoryReservation'] = utils.parse_bytes(mem_reservation)
if memswap_limit: data = resources.copy()
data['MemorySwap'] = utils.parse_bytes(memswap_limit)
if kernel_memory:
data['KernelMemory'] = utils.parse_bytes(kernel_memory)
if restart_policy: if restart_policy:
if utils.version_lt(self._version, '1.23'): if utils.version_lt(self._version, '1.23'):
raise errors.InvalidVersion( raise errors.InvalidVersion(
@ -1275,8 +1359,8 @@ class ContainerApiMixin:
'for API version < 1.23' 'for API version < 1.23'
) )
data['RestartPolicy'] = restart_policy data['RestartPolicy'] = restart_policy
print(data)
res = self._post_json(url, data=data) res = self._post_json(u, data=data)
return self._result(res, True) return self._result(res, True)
@utils.check_resource('container') @utils.check_resource('container')

View File

@ -9,6 +9,7 @@ from ..errors import (
NotFound, create_unexpected_kwargs_error NotFound, create_unexpected_kwargs_error
) )
from ..types import HostConfig from ..types import HostConfig
from ..types import HostResources
from ..utils import version_gte from ..utils import version_gte
from .images import Image from .images import Image
from .resource import Collection, Model from .resource import Collection, Model
@ -466,18 +467,66 @@ class Container(Model):
Update resource configuration of the containers. Update resource configuration of the containers.
Args: Args:
blkio_weight (int): Block IO (relative weight), between 10 and 1000
cpu_period (int): Limit CPU CFS (Completely Fair Scheduler) period blkio_weight_device: Block IO weight (relative device weight) in
cpu_quota (int): Limit CPU CFS (Completely Fair Scheduler) quota the form of: ``[{"Path": "device_path", "Weight": weight}]``.
cpu_shares (int): CPU shares (relative weight) blkio_weight: Block IO weight (relative weight), accepts a weight
cpuset_cpus (str): CPUs in which to allow execution value between 10 and 1000.
cpuset_mems (str): MEMs in which to allow execution cgroup_parent (string): Path to `cgroups` under which the
mem_limit (int or str): Memory limit container's `cgroup` is created. If the path is not absolute,
mem_reservation (int or str): Memory soft limit the path is considered to be relative to the `cgroups` path of
memswap_limit (int or str): Total memory (memory + swap), -1 to the init process. Cgroups are created if they do not already
disable swap exist.
cpu_period (int): The length of a CPU period in microseconds.
cpu_quota (int): Microseconds of CPU time that the container can
get in a CPU period.
cpu_rt_period (int): The length of a CPU real-time period in
microseconds. Set to 0 to allocate no time allocated to
real-time tasks.
cpu_rt_runtime (int): The length of a CPU real-time runtime in
microseconds. Set to 0 to allocate no time allocated to
real-time tasks.
cpu_shares (int): CPU shares (relative weight).
cpuset_cpus (str): CPUs in which to allow execution (``0-3``,
``0,1``).
cpuset_mems (str): Memory nodes (MEMs) in which to allow execution
(``0-3``, ``0,1``). Only effective on NUMA systems.
device_cgroup_rules (:py:class:`list`): A list of cgroup rules to
apply to the container.
device_read_bps: Limit read rate (bytes per second) from a device
in the form of: `[{"Path": "device_path", "Rate": rate}]`
device_read_iops: Limit read rate (IO per second) from a device.
device_write_bps: Limit write rate (bytes per second) from a
device.
device_write_iops: Limit write rate (IO per second) from a device.
devices (:py:class:`list`): Expose host devices to the container,
as a list of strings in the form
``<path_on_host>:<path_in_container>:<cgroup_permissions>``.
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
to have read-write access to the host's ``/dev/sda`` via a
node named ``/dev/xvda`` inside the container.
device_requests (:py:class:`list`): Expose host resources such as
GPUs to the container, as a list of
:py:class:`docker.types.DeviceRequest` instances.
kernel_memory (int or str): Kernel memory limit kernel_memory (int or str): Kernel memory limit
mem_limit (float or str): Memory limit. Accepts float values
(which represent the memory limit of the created container in
bytes) or a string with a units identification char
(``100000b``, ``1000k``, ``128m``, ``1g``). If a string is
specified without a units character, bytes are assumed as an
mem_reservation (float or str): Memory soft limit.
mem_swappiness (int): Tune a container's memory swappiness
behavior. Accepts number between 0 and 100.
memswap_limit (str or int): Maximum amount of memory + swap a
container is allowed to consume.
nano_cpus (int): CPU quota in units of 10<sup>-9</sup> CPUs.
oom_kill_disable (bool): Whether to disable OOM killer.
pids_limit (int): Tune a container's pids limit. Set ``-1`` for
unlimited.
restart_policy (dict): Restart policy dictionary restart_policy (dict): Restart policy dictionary
ulimits (:py:class:`list`): Ulimits to set inside the container,
as a list of :py:class:`docker.types.Ulimit` instances.
Returns: Returns:
(dict): Dictionary containing a ``Warnings`` key. (dict): Dictionary containing a ``Warnings`` key.
@ -486,7 +535,14 @@ class Container(Model):
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
""" """
return self.client.api.update_container(self.id, **kwargs) resources_kwargs = copy.copy(kwargs)
resources_kwargs.pop("restart_policy", None)
resources_kwargs['version'] = self.client.api._version
return self.client.api.update_container_from_resources(
self.id, resources=HostResources(**resources_kwargs),
restart_policy=kwargs.get("restart_policy", None)
)
def wait(self, **kwargs): def wait(self, **kwargs):
""" """

View File

@ -434,6 +434,20 @@ class ContainerTest(BaseIntegrationTest):
container.reload() container.reload()
assert container.attrs['HostConfig']['CpuShares'] == 3 assert container.attrs['HostConfig']['CpuShares'] == 3
def test_update_device_cgroup_rules(self):
client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.run("alpine", "sleep 60", detach=True,
device_cgroup_rules=["c 13:* rwm"])
self.tmp_containers.append(container.id)
device_cgrp_rules = container.attrs['HostConfig']['DeviceCgroupRules']
assert len(device_cgrp_rules) == 1
assert device_cgrp_rules[0] == "c 13:* rwm"
container.update(device_cgroup_rules=["c 13:* rw"])
container.reload()
device_cgrp_rules_new = container.attrs['HostConfig']['DeviceCgroupRules']
assert len(device_cgrp_rules_new) == 1
assert device_cgrp_rules_new[0] == "c 13:* rw"
def test_wait(self): def test_wait(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.run("alpine", "sh -c 'exit 0'", container = client.containers.run("alpine", "sh -c 'exit 0'",

View File

@ -582,8 +582,11 @@ class ContainerTest(unittest.TestCase):
client = make_fake_client() client = make_fake_client()
container = client.containers.get(FAKE_CONTAINER_ID) container = client.containers.get(FAKE_CONTAINER_ID)
container.update(cpu_shares=2) container.update(cpu_shares=2)
client.api.update_container.assert_called_with(FAKE_CONTAINER_ID, client.api.update_container_from_resources.assert_called_with(
cpu_shares=2) FAKE_CONTAINER_ID,
resources={'CpuShares': 2},
restart_policy=None
)
def test_wait(self): def test_wait(self):
client = make_fake_client() client = make_fake_client()