diff --git a/compose/config/config.py b/compose/config/config.py index f1195c8ec..4fddac822 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -38,6 +38,7 @@ from .types import VolumeSpec from .validation import match_named_volumes from .validation import validate_against_config_schema from .validation import validate_config_section +from .validation import validate_cpu from .validation import validate_depends_on from .validation import validate_extends_file_path from .validation import validate_links @@ -52,8 +53,11 @@ DOCKER_CONFIG_KEYS = [ 'cap_drop', 'cgroup_parent', 'command', + 'cpu_count', + 'cpu_percent', 'cpu_quota', 'cpu_shares', + 'cpus', 'cpuset', 'detach', 'devices', @@ -640,6 +644,7 @@ def validate_service(service_config, service_names, config_file): validate_service_constraints(service_dict, service_name, config_file) validate_paths(service_dict) + validate_cpu(service_config) validate_ulimits(service_config) validate_network_mode(service_config, service_names) validate_depends_on(service_config, service_names) diff --git a/compose/config/config_schema_v2.2.json b/compose/config/config_schema_v2.2.json index a178fccc4..a585f2a8c 100644 --- a/compose/config/config_schema_v2.2.json +++ b/compose/config/config_schema_v2.2.json @@ -74,8 +74,11 @@ ] }, "container_name": {"type": "string"}, + "cpu_count": {"type": "integer", "minimum": 0}, + "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, "cpu_shares": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]}, + "cpus": {"type": "number", "minimum": 0}, "cpuset": {"type": "string"}, "depends_on": { "oneOf": [ diff --git a/compose/config/validation.py b/compose/config/validation.py index 1df6dd6b7..856f811c5 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -15,6 +15,7 @@ from jsonschema import RefResolver from jsonschema import ValidationError from ..const import COMPOSEFILE_V1 as V1 +from ..const import NANOCPUS_SCALE from .errors import ConfigurationError from .errors import VERSION_EXPLANATION from .sort_services import get_service_name_from_network_mode @@ -387,6 +388,16 @@ def validate_service_constraints(config, service_name, config_file): handle_errors(validator.iter_errors(config), handler, None) +def validate_cpu(service_config): + cpus = service_config.config.get('cpus') + if not cpus: + return + nano_cpus = cpus * NANOCPUS_SCALE + if isinstance(nano_cpus, float) and not nano_cpus.is_integer(): + raise ConfigurationError( + "cpus must have nine or less digits after decimal point") + + def get_schema_path(): return os.path.dirname(os.path.abspath(__file__)) diff --git a/compose/const.py b/compose/const.py index 573136d5d..36703138a 100644 --- a/compose/const.py +++ b/compose/const.py @@ -15,6 +15,7 @@ LABEL_NETWORK = 'com.docker.compose.network' LABEL_VERSION = 'com.docker.compose.version' LABEL_VOLUME = 'com.docker.compose.volume' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' +NANOCPUS_SCALE = 1000000000 SECRETS_PATH = '/run/secrets' diff --git a/compose/service.py b/compose/service.py index edd0a3764..19873d5e5 100644 --- a/compose/service.py +++ b/compose/service.py @@ -34,6 +34,7 @@ from .const import LABEL_ONE_OFF from .const import LABEL_PROJECT from .const import LABEL_SERVICE from .const import LABEL_VERSION +from .const import NANOCPUS_SCALE from .container import Container from .errors import HealthCheckFailed from .errors import NoHealthCheckConfigured @@ -52,7 +53,10 @@ HOST_CONFIG_KEYS = [ 'cap_add', 'cap_drop', 'cgroup_parent', + 'cpu_count', + 'cpu_percent', 'cpu_quota', + 'cpus', 'devices', 'dns', 'dns_search', @@ -798,6 +802,10 @@ class Service(object): init_path = options.get('init') options['init'] = True + nano_cpus = None + if 'cpus' in options: + nano_cpus = int(options.get('cpus') * NANOCPUS_SCALE) + return self.client.create_host_config( links=self._get_links(link_to_self=one_off), port_bindings=build_port_bindings( @@ -837,6 +845,9 @@ class Service(object): init=options.get('init', None), init_path=init_path, isolation=options.get('isolation'), + cpu_count=options.get('cpu_count'), + cpu_percent=options.get('cpu_percent'), + nano_cpus=nano_cpus, ) def get_secret_volumes(self): diff --git a/setup.py b/setup.py index 19a0d4aa0..8dbb337cc 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ install_requires = [ 'requests >= 2.6.1, != 2.11.0, < 2.12', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker >= 2.2.1, < 3.0', + 'docker >= 2.3.0, < 3.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 87549c506..a5b5bda57 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -19,6 +19,7 @@ from .testcases import pull_busybox from compose import __version__ from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec +from compose.const import IS_WINDOWS_PLATFORM from compose.const import LABEL_CONFIG_HASH from compose.const import LABEL_CONTAINER_NUMBER from compose.const import LABEL_ONE_OFF @@ -33,6 +34,7 @@ from compose.service import ConvergenceStrategy from compose.service import NetworkMode from compose.service import Service from tests.integration.testcases import v2_1_only +from tests.integration.testcases import v2_2_only from tests.integration.testcases import v2_only from tests.integration.testcases import v3_only @@ -110,6 +112,31 @@ class ServiceTest(DockerClientTestCase): container.start() self.assertEqual(container.get('HostConfig.CpuQuota'), 40000) + @v2_2_only() + def test_create_container_with_cpu_count(self): + self.require_api_version('1.25') + service = self.create_service('db', cpu_count=2) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.CpuCount'), 2) + + @v2_2_only() + @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='cpu_percent is not supported for Linux') + def test_create_container_with_cpu_percent(self): + self.require_api_version('1.25') + service = self.create_service('db', cpu_percent=12) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.CpuPercent'), 12) + + @v2_2_only() + def test_create_container_with_cpus(self): + self.require_api_version('1.25') + service = self.create_service('db', cpus=1) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.NanoCpus'), 1000000000) + def test_create_container_with_shm_size(self): self.require_api_version('1.22') service = self.create_service('db', shm_size=67108864) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index a5fe999d9..57814872c 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -15,6 +15,7 @@ from compose.const import API_VERSIONS from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_0 as V2_1 +from compose.const import COMPOSEFILE_V2_2 as V2_2 from compose.const import COMPOSEFILE_V3_2 as V3_2 from compose.const import LABEL_PROJECT from compose.progress_stream import stream_output @@ -70,10 +71,14 @@ def v2_1_only(): return build_version_required_decorator((V1, V2_0)) -def v3_only(): +def v2_2_only(): return build_version_required_decorator((V1, V2_0, V2_1)) +def v3_only(): + return build_version_required_decorator((V1, V2_0, V2_1, V2_2)) + + class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index d3087fffe..e66e952f8 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -27,6 +27,7 @@ from compose.config.types import VolumeSpec from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_1 as V2_1 +from compose.const import COMPOSEFILE_V2_2 as V2_2 from compose.const import COMPOSEFILE_V3_0 as V3_0 from compose.const import COMPOSEFILE_V3_1 as V3_1 from compose.const import COMPOSEFILE_V3_2 as V3_2 @@ -174,6 +175,9 @@ class ConfigTest(unittest.TestCase): cfg = config.load(build_config_details({'version': '2.1'})) assert cfg.version == V2_1 + cfg = config.load(build_config_details({'version': '2.2'})) + assert cfg.version == V2_2 + for version in ['3', '3.0']: cfg = config.load(build_config_details({'version': version})) assert cfg.version == V3_0