diff --git a/salt/modules/dockerng.py b/salt/modules/dockerng.py index 896a50ff98..d30e47b27d 100644 --- a/salt/modules/dockerng.py +++ b/salt/modules/dockerng.py @@ -417,6 +417,9 @@ VALID_CREATE_OPTS = { 'cpuset': { 'path': 'Config:Cpuset', }, + 'labels': { + 'path': 'Config:Labels', + }, } VALID_RUNTIME_OPTS = { @@ -1110,6 +1113,13 @@ def _validate_input(action, for x in kwargs[key]]): raise SaltInvocationError(key + ' must be a list of strings') + def _valid_dictlist(key): # pylint: disable=unused-variable + ''' + Ensure the passed value is a list of dictionaries. + ''' + if not salt.utils.is_dictlist(kwargs[key]): + raise SaltInvocationError(key + ' must be a list of dictionaries.') + # Custom validation functions for container creation options def _valid_command(): # pylint: disable=unused-variable ''' @@ -1644,6 +1654,32 @@ def _validate_input(action, 'pid_mode can only be \'host\', if set' ) + def _valid_labels(): # pylint: disable=unused-variable + ''' + Must be a dict or a list of strings + ''' + if kwargs.get('labels') is None: + return + try: + _valid_stringlist('labels') + except SaltInvocationError: + try: + _valid_dictlist('labels') + except SaltInvocationError: + try: + _valid_dict('labels') + except SaltInvocationError: + raise SaltInvocationError( + 'labels can only be a list of strings/dict' + ' or a dict containing strings') + else: + new_labels = {} + for k, v in six.iteritems(kwargs['labels']): + new_labels[str(k)] = str(v) + kwargs['labels'] = new_labels + else: + kwargs['labels'] = salt.utils.repack_dictlist(kwargs['labels']) + # And now, the actual logic to perform the validation if action == 'create': valid_opts = VALID_CREATE_OPTS @@ -2640,6 +2676,12 @@ def create(image, This is only used if Salt needs to pull the requested image. + labels + Add Metadata to the container. Can be a list of strings/dictionaries + or a dictionary of strings (keys and values). + + Example: ``labels=LABEL1,LABEL2``, + ``labels="{'LABEL1': 'value1', 'LABEL2': 'value2'}"`` **RETURN DATA** diff --git a/salt/states/dockerng.py b/salt/states/dockerng.py index 98ceb73a68..cc17257080 100644 --- a/salt/states/dockerng.py +++ b/salt/states/dockerng.py @@ -1349,6 +1349,37 @@ def running(name, This option requires Docker 1.5.0 or newer. + labels + Add Metadata to the container. Can be a list of strings/dictionaries + or a dictionary of strings (keys and values). + + .. code-block:: yaml + + foo: + dockerng.running: + - image: bar/baz:latest + - labels: + - LABEL1 + - LABEL2 + + .. code-block:: yaml + + foo: + dockerng.running: + - image: bar/baz:latest + - labels: + KEY1: VALUE1 + KEY2: VALUE2 + + .. code-block:: yaml + + foo: + dockerng.running: + - image: bar/baz:latest + - labels: + - KEY1: VALUE1 + - KEY2: VALUE2 + start : True Set to ``False`` to suppress starting of the container if it exists, matches the desired configuration, but is not running. This is useful diff --git a/tests/unit/modules/dockerng_test.py b/tests/unit/modules/dockerng_test.py index 2c940f13b5..b1d3abf08a 100644 --- a/tests/unit/modules/dockerng_test.py +++ b/tests/unit/modules/dockerng_test.py @@ -21,6 +21,7 @@ ensure_in_syspath('../../') # Import Salt Libs from salt.modules import dockerng as dockerng_mod +from salt.exceptions import SaltInvocationError dockerng_mod.__context__ = {'docker.docker_version': ''} dockerng_mod.__salt__ = {} @@ -139,6 +140,141 @@ class DockerngTestCase(TestCase): image='image', name='ctn') + @skipIf(_docker_py_version() < (1, 4, 0), + 'docker module must be installed to run this test or is too old. >=1.4.0') + @patch.object(dockerng_mod, 'images', MagicMock()) + @patch.object(dockerng_mod, 'inspect_image') + @patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'})) + def test_create_with_labels_dict(self, *args): + ''' + Create container with labels dictionary. + ''' + __salt__ = { + 'config.get': Mock(), + 'mine.send': Mock(), + } + host_config = {} + client = Mock() + client.api_version = '1.19' + client.create_host_config.return_value = host_config + client.create_container.return_value = {} + with patch.dict(dockerng_mod.__dict__, + {'__salt__': __salt__}): + with patch.dict(dockerng_mod.__context__, + {'docker.client': client}): + dockerng_mod.create( + 'image', + name='ctn', + labels={'KEY': 'VALUE'}, + validate_input=True, + ) + client.create_container.assert_called_once_with( + labels={'KEY': 'VALUE'}, + host_config=host_config, + image='image', + name='ctn', + ) + + @skipIf(_docker_py_version() < (1, 4, 0), + 'docker module must be installed to run this test or is too old. >=1.4.0') + @patch.object(dockerng_mod, 'images', MagicMock()) + @patch.object(dockerng_mod, 'inspect_image') + @patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'})) + def test_create_with_labels_list(self, *args): + ''' + Create container with labels list. + ''' + __salt__ = { + 'config.get': Mock(), + 'mine.send': Mock(), + } + host_config = {} + client = Mock() + client.api_version = '1.19' + client.create_host_config.return_value = host_config + client.create_container.return_value = {} + with patch.dict(dockerng_mod.__dict__, + {'__salt__': __salt__}): + with patch.dict(dockerng_mod.__context__, + {'docker.client': client}): + dockerng_mod.create( + 'image', + name='ctn', + labels=['KEY1', 'KEY2'], + validate_input=True, + ) + client.create_container.assert_called_once_with( + labels=['KEY1', 'KEY2'], + host_config=host_config, + image='image', + name='ctn', + ) + + @skipIf(_docker_py_version() < (1, 4, 0), + 'docker module must be installed to run this test or is too old. >=1.4.0') + @patch.object(dockerng_mod, 'images', MagicMock()) + @patch.object(dockerng_mod, 'inspect_image') + @patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'})) + def test_create_with_labels_error(self, *args): + ''' + Create container with invalid labels. + ''' + __salt__ = { + 'config.get': Mock(), + 'mine.send': Mock(), + } + host_config = {} + client = Mock() + client.api_version = '1.19' + client.create_host_config.return_value = host_config + client.create_container.return_value = {} + with patch.dict(dockerng_mod.__dict__, + {'__salt__': __salt__}): + with patch.dict(dockerng_mod.__context__, + {'docker.client': client}): + self.assertRaises(SaltInvocationError, + dockerng_mod.create, + 'image', + name='ctn', + labels=22, + validate_input=True, + ) + + @skipIf(_docker_py_version() < (1, 4, 0), + 'docker module must be installed to run this test or is too old. >=1.4.0') + @patch.object(dockerng_mod, 'images', MagicMock()) + @patch.object(dockerng_mod, 'inspect_image') + @patch.object(dockerng_mod, 'version', Mock(return_value={'ApiVersion': '1.19'})) + def test_create_with_labels_dictlist(self, *args): + ''' + Create container with labels dictlist. + ''' + __salt__ = { + 'config.get': Mock(), + 'mine.send': Mock(), + } + host_config = {} + client = Mock() + client.api_version = '1.19' + client.create_host_config.return_value = host_config + client.create_container.return_value = {} + with patch.dict(dockerng_mod.__dict__, + {'__salt__': __salt__}): + with patch.dict(dockerng_mod.__context__, + {'docker.client': client}): + dockerng_mod.create( + 'image', + name='ctn', + labels=[{'KEY1': 'VALUE1'}, {'KEY2': 'VALUE2'}], + validate_input=True, + ) + client.create_container.assert_called_once_with( + labels={'KEY1': 'VALUE1', 'KEY2': 'VALUE2'}, + host_config=host_config, + image='image', + name='ctn', + ) + if __name__ == '__main__': from integration import run_tests diff --git a/tests/unit/states/dockerng_test.py b/tests/unit/states/dockerng_test.py index 43fecb45d0..b84aee100c 100644 --- a/tests/unit/states/dockerng_test.py +++ b/tests/unit/states/dockerng_test.py @@ -434,6 +434,32 @@ class DockerngTestCase(TestCase): 'name': 'cont', 'result': False}) + def test_running_with_labels(self): + ''' + Test dockerng.running with labels parameter. + ''' + dockerng_create = Mock() + __salt__ = {'dockerng.list_containers': MagicMock(), + 'dockerng.list_tags': MagicMock(), + 'dockerng.pull': MagicMock(), + 'dockerng.state': MagicMock(), + 'dockerng.inspect_image': MagicMock(), + 'dockerng.create': dockerng_create, + } + with patch.dict(dockerng_state.__dict__, + {'__salt__': __salt__}): + dockerng_state.running( + 'cont', + image='image:latest', + labels=['LABEL1', 'LABEL2'], + ) + dockerng_create.assert_called_with( + 'image:latest', + validate_input=False, + name='cont', + labels=['LABEL1', 'LABEL2'], + client_timeout=60) + if __name__ == '__main__': from integration import run_tests