{"action":"created","comment":{"url":"https://api.github.com/repos/saltstack/salt/pulls/comments/28201063","id":28201063,"diff_hunk":"@@ -0,0 +1,1495 @@\n+# -*- coding: utf-8 -*-\n+'''\n+Management of Docker containers\n+\n+.. versionadded:: Beryllium\n+\n+\n+This is the state module to accompany the :mod:`docker-ng\n+<salt.modules.dockerng>` execution module.\n+\n+.. note::\n+\n+    To pull from a Docker registry, authentication must be configured. See\n+    :ref:`here <docker-authentication>` for more information on how to\n+    configure access to docker registries in :ref:`Pillar <pillar>` data.\n+'''\n+\n+from __future__ import absolute_import\n+import logging\n+import sys\n+import time\n+import traceback\n+\n+# Import salt libs\n+from salt.exceptions import CommandExecutionError, SaltInvocationError\n+# pylint: disable=no-name-in-module,import-error\n+from salt.modules.dockerng import (\n+    CLIENT_TIMEOUT,\n+    VALID_CREATE_OPTS,\n+    VALID_RUNTIME_OPTS,\n+    _validate_input,\n+    _get_repo_tag\n+)\n+# pylint: enable=no-name-in-module,import-error\n+import salt.utils\n+import salt.ext.six as six\n+\n+# Enable proper logging\n+log = logging.getLogger(__name__)  # pylint: disable=invalid-name\n+\n+# Define the module's virtual name\n+__virtualname__ = 'docker-ng'\n+\n+NOTSET = object()\n+\n+\n+def __virtual__():\n+    '''\n+    Only load if the dockerio execution module is available\n+    '''\n+    if 'docker-ng.version' in __salt__:\n+        global _validate_input  # pylint: disable=global-statement\n+        _validate_input = salt.utils.namespaced_function(\n+            _validate_input, globals()\n+        )\n+        return __virtualname__\n+    return False\n+\n+\n+def _format_comments(comments):\n+    '''\n+    DRY code for joining comments together and conditionally adding a period at\n+    the end.\n+    '''\n+    ret = '. '.join(comments)\n+    if len(comments) > 1:\n+        ret += '.'\n+    return ret\n+\n+\n+def _api_mismatch(param):\n+    '''\n+    Raise an exception if a config value can't be found at the expected\n+    location in a call to docker-ng.inspect_container\n+    '''\n+    raise CommandExecutionError(\n+        'Unable to compare configuration for the \\'{0}\\' parameter. This may '\n+        'be due to a change in the Docker API'.format(param)\n+    )\n+\n+\n+def _prep_input(kwargs):\n+    '''\n+    Repack (if necessary) data that should be in a dict but is easier to\n+    configure in an SLS file as a dictlist. If the data type is a string, then\n+    skip repacking and let _validate_input() try to sort it out.\n+    '''\n+    for kwarg in ('environment', 'lxc_conf'):\n+        kwarg_value = kwargs.get(kwarg)\n+        if kwarg_value is not None \\\n+                and not isinstance(kwarg_value, six.string_types):\n+            err = ('Invalid {0} configuration. See the documentation for '\n+                   'proper usage.'.format(kwarg))\n+            if salt.utils.is_dictlist(kwarg_value):\n+                new_kwarg_value = salt.utils.repack_dictlist(kwarg_value)\n+                if not kwarg_value:\n+                    raise SaltInvocationError(err)\n+                kwargs[kwarg] = new_kwarg_value\n+            if not isinstance(kwargs[kwarg], dict):\n+                raise SaltInvocationError(err)\n+\n+\n+def _compare(actual, create_kwargs, runtime_kwargs):\n+    '''\n+    Compare the desired configuration against the actual configuration returned\n+    by docker-ng.inspect_container\n+    '''\n+    _get = lambda path: (\n+        salt.utils.traverse_dict(actual, path, NOTSET, delimiter=':')\n+    )\n+    ret = {}\n+    for desired, valid_opts in ((create_kwargs, VALID_CREATE_OPTS),\n+                                (runtime_kwargs, VALID_RUNTIME_OPTS)):\n+        for item, data, in six.iteritems(desired):\n+            if item not in valid_opts:\n+                log.error(\n+                    'Trying to compare \\'{0}\\', but it is not a valid '\n+                    'parameter. Skipping.'.format(item)\n+                )\n+                continue\n+            log.trace('docker-ng.running: comparing ' + item)\n+            conf_path = valid_opts[item]['path']\n+            if isinstance(conf_path, tuple):\n+                actual_data = [_get(x) for x in conf_path]\n+                for val in actual_data:\n+                    if val is NOTSET:\n+                        _api_mismatch(item)\n+            else:\n+                actual_data = _get(conf_path)\n+                if actual_data is NOTSET:\n+                    _api_mismatch(item)\n+            log.trace('docker-ng.running ({0}): desired value: {1}'\n+                      .format(item, data))\n+            log.trace('docker-ng.running ({0}): actual value: {1}'\n+                      .format(item, actual_data))\n+\n+            if actual_data is None and data is not None \\\n+                    or actual_data is not None and data is None:\n+                ret.update({item: {'old': actual_data, 'new': data}})\n+                continue\n+\n+            # 'create' comparison params\n+            if item == 'detach':\n+                # Something unique here. Two fields to check, if both are False\n+                # then detach is True\n+                actual_detach = all(x is False for x in actual_data)\n+                log.trace('docker-ng.running ({0}): munged actual value: {1}'\n+                          .format(item, actual_detach))\n+                if actual_detach != data:\n+                    ret.update({item: {'old': actual_detach, 'new': data}})\n+                continue\n+\n+            elif item == 'environment':\n+                actual_env = {}\n+                for env_var in actual_data:\n+                    try:\n+                        key, val = env_var.split('=', 1)\n+                    except (AttributeError, ValueError):\n+                        log.warning(\n+                            'Unexpected environment variable in inspect '\n+                            'output {0}'.format(env_var)\n+                        )\n+                        continue\n+                    else:\n+                        actual_env[key] = val\n+                log.trace('docker-ng.running ({0}): munged actual value: {1}'\n+                          .format(item, actual_env))\n+                env_diff = {}\n+                for key in data:\n+                    actual_val = actual_env.get(key)\n+                    if data[key] != actual_val:\n+                        env_ptr = env_diff.setdefault(item, {})\n+                        env_ptr.setdefault('old', {})[key] = actual_val\n+                        env_ptr.setdefault('new', {})[key] = data[key]\n+                if env_diff:\n+                    ret.update(env_diff)\n+                continue\n+\n+            elif item == 'ports':\n+                # Munge the desired configuration instead of the actual\n+                # configuration here, because the desired configuration is a\n+                # list of ints or tuples, and that won't look as good in the\n+                # nested outputter as a simple comparison of lists of\n+                # port/protocol pairs (as found in the \"actual\" dict).\n+                actual_ports = sorted(actual_data)\n+                desired_ports = []\n+                for port_def in data:\n+                    if isinstance(port_def, tuple):\n+                        desired_ports.append('{0}/{1}'.format(*port_def))\n+                    else:\n+                        desired_ports.append('{0}/tcp'.format(port_def))\n+                desired_ports.sort()\n+                log.trace('docker-ng.running ({0}): munged actual value: {1}'\n+                          .format(item, actual_ports))\n+                log.trace('docker-ng.running ({0}): munged desired value: {1}'\n+                          .format(item, desired_ports))\n+                if actual_ports != desired_ports:\n+                    ret.update({item: {'old': actual_ports,\n+                                       'new': desired_ports}})\n+                continue\n+\n+            # 'runtime' comparison params\n+            elif item == 'binds':\n+                actual_binds = []\n+                for bind in actual_data:\n+                    bind_parts = bind.split(':')\n+                    if len(bind_parts) == 2:\n+                        actual_binds.append(bind + ':rw')\n+                    else:\n+                        actual_binds.append(bind)\n+                desired_binds = []\n+                for host_path, bind_data in six.iteritems(data):\n+                    desired_binds.append(\n+                        '{0}:{1}:{2}'.format(\n+                            host_path,\n+                            bind_data['bind'],\n+                            'ro' if bind_data['ro'] else 'rw'\n+                        )\n+                    )\n+                actual_binds.sort()\n+                desired_binds.sort()\n+                if actual_binds != desired_binds:\n+                    ret.update({item: {'old': actual_binds,\n+                                       'new': desired_binds}})\n+                    continue\n+\n+            elif item == 'port_bindings':\n+                actual_binds = []\n+                for container_port, bind_list in six.iteritems(actual_data):\n+                    if container_port.endswith('/tcp'):\n+                        container_port = container_port[:-4]\n+                    for bind_data in bind_list:\n+                        # Port range will have to be updated for future Docker\n+                        # versions (see\n+                        # https://github.com/docker/docker/issues/10220).  Note\n+                        # that Docker 1.5.0 (released a few weeks after the fix\n+                        # was merged) does not appear to have this fix in it,\n+                        # so we're probably looking at 1.6.0 for this fix.\n+                        if bind_data['HostPort'] == '' or \\\n+                                49153 <= int(bind_data['HostPort']) <= 65535:\n+                            host_port = ''\n+                        else:\n+                            host_port = bind_data['HostPort']\n+                        if bind_data['HostIp'] in ('0.0.0.0', ''):\n+                            if host_port:\n+                                bind_def = (host_port, container_port)\n+                            else:\n+                                bind_def = (container_port,)\n+                        else:\n+                            bind_def = (bind_data['HostIp'],\n+                                        host_port,\n+                                        container_port)\n+                        actual_binds.append(':'.join(bind_def))\n+\n+                desired_binds = []\n+                for container_port, bind_list in six.iteritems(data):\n+                    try:\n+                        if container_port.endswith('/tcp'):\n+                            container_port = container_port[:-4]\n+                    except AttributeError:\n+                        # The port's protocol was not specified, so it is\n+                        # assumed to be TCP. Thus, according to docker-py usage\n+                        # examples, the port was passed as an int. Convert it\n+                        # to a string here.\n+                        container_port = str(container_port)\n+                    for bind_data in bind_list:\n+                        if isinstance(bind_data, tuple):\n+                            try:\n+                                host_ip, host_port = bind_data\n+                                host_port = str(host_port)\n+                            except ValueError:\n+                                host_ip = bind_data[0]\n+                                host_port = ''\n+                            bind_def = '{0}:{1}:{2}'.format(\n+                                host_ip, host_port, container_port\n+                            )\n+                        else:\n+                            if bind_data is not None:\n+                                bind_def = '{0}:{1}'.format(\n+                                    bind_data, container_port\n+                                )\n+                            else:\n+                                bind_def = container_port\n+                        desired_binds.append(bind_def)\n+                actual_binds.sort()\n+                desired_binds.sort()\n+                log.trace('docker-ng.running ({0}): munged actual value: {1}'\n+                          .format(item, actual_binds))\n+                log.trace('docker-ng.running ({0}): munged desired value: {1}'\n+                          .format(item, desired_binds))\n+                if actual_binds != desired_binds:\n+                    ret.update({item: {'old': actual_binds,\n+                                       'new': desired_binds}})\n+                    continue\n+\n+            elif item == 'links':\n+                actual_links = []\n+                for link in actual_data:\n+                    try:\n+                        link_name, alias_info = link.split(':')\n+                    except ValueError:\n+                        log.error(\n+                            'Failed to compare link {0}, unrecognized format'\n+                            .format(link)\n+                        )\n+                        continue\n+                    container_name, _, link_alias = alias_info.rpartition('/')\n+                    if not container_name:\n+                        log.error(\n+                            'Failed to interpret link alias from {0}, '\n+                            'unrecognized format'.format(alias_info)\n+                        )\n+                        continue\n+                    actual_links.append((link_name, link_alias))\n+                actual_links.sort()\n+                desired_links = sorted(data)\n+                if actual_links != desired_links:\n+                    ret.update({item: {'old': actual_links,\n+                                       'new': desired_links}})\n+                    continue\n+\n+            elif item == 'extra_hosts':\n+                actual_hosts = sorted(actual_data)\n+                desired_hosts = sorted(\n+                    ['{0}:{1}'.format(x, y) for x, y in six.iteritems(data)]\n+                )\n+                if actual_hosts != desired_hosts:\n+                    ret.update({item: {'old': actual_hosts,\n+                                       'new': desired_hosts}})\n+                    continue\n+\n+            elif isinstance(data, list):\n+                # Compare two sorted lists of items. Won't work for \"command\"\n+                # or \"entrypoint\" because those are both shell commands and the\n+                # original order matters. It will, however, work for \"volumes\"\n+                # because even though \"volumes\" is a sub-dict nested within the\n+                # \"actual\" dict sorted(somedict) still just gives you a sorted\n+                # list of the dictionary's keys. And we don't care about the\n+                # value for \"volumes\", just its keys.\n+                actual_data = sorted(actual_data)\n+                desired_data = sorted(data)\n+                log.trace('docker-ng.running ({0}): munged actual value: {1}'\n+                          .format(item, actual_data))\n+                log.trace('docker-ng.running ({0}): munged desired value: {1}'\n+                          .format(item, desired_data))\n+                if actual_data != desired_data:\n+                    ret.update({item: {'old': actual_data,\n+                                       'new': desired_data}})\n+                continue\n+\n+            else:\n+                # Generic comparison, works on strings, numeric types, and\n+                # booleans\n+                if actual_data != data:\n+                    ret.update({item: {'old': actual_data, 'new': data}})\n+    return ret\n+\n+\n+def running(name,\n+            pull=None,\n+            build=None,\n+            load=None,\n+            update=False,\n+            force=False,\n+            stop_timeout=10,\n+            insecure_registry=False,\n+            validate_ip_addrs=True,\n+            client_timeout=CLIENT_TIMEOUT,\n+            **kwargs):\n+    '''\n+    Ensures that a container is running\n+\n+    name\n+        Name of the container\n+\n+    pull\n+        Name or ID of an image from a Docker registry. Image names can be\n+        specified either using ``repo:tag`` notation, or just the repo name (in\n+        which case a tag of ``latest`` is assumed). If the image is not\n+        present, it will be pulled.\n+\n+        If the specified image already exists, it will not be pulled unless\n+        ``update`` is set to ``True``.\n+\n+        .. code-block:: yaml\n+\n+            mycontainer:\n+              docker-ng.running:\n+                - pull: myuser/myimage:mytag\n+    build\n+        Path to directory on the Minion containing a Dockerfile\n+\n+        .. code-block:: yaml\n+\n+            mycontainer:\n+              docker-ng.running:\n+                - build:\n+                    - myuser/myimage:mytag: /home/myuser/docker/myimage\n+\n+        The image will be built using :py:func:`docker-ng.build\n+        <salt.modules.dockerng.build>` and the specified image name and tag\n+        applied to it.\n+\n+        If the specified image already exists, then the build will not be\n+        performed unless ``update`` is set to ``True``.\n+\n+    load\n+        Loads a tar archive created with :py:func:`docker-ng.load\n+        <salt.modules.dockerng.load>` (or the ``docker load`` Docker CLI\n+        command). Here's a usage example:\n+\n+        .. code-block:: yaml\n+\n+            mycontainer:\n+              docker-ng.running:\n+                - load:\n+                  - myuser/myimage:mytag: salt://path/to/image.tar\n+\n+        The image will be loaded using :py:func:`docker-ng.load\n+        <salt.modules.dockerng.load>`, and then tagged.\n+\n+        If the specified image already exists, then the build will not be\n+        performed unless ``update`` is set to ``True``.\n+\n+    update : False\n+        Set this parameter to ``True`` to make Salt pull/build/load the image\n+        even if the named container exists and matches the specified image. If\n+        the pull/build/load results in a new ID for the image, then the\n+        container will be replaced with the updated image.\n+\n+    force : False\n+        Set this parameter to ``True`` to force Salt to pull/build/load the\n+        image and replace the container.\n+\n+        .. note::\n+\n+            This option can also be overridden by Pillar data. If the Minion\n+            has a pillar variable named ``docker-ng.running.force`` which is\n+            set to ``True``, it will turn on this option. This pillar variable\n+            can even be set at runtime. For example:\n+\n+            .. code-block:: bash\n+\n+                salt myminion state.sls docker_stuff pillar=\"{docker-ng.running.force: True}\"\n+\n+            If this pillar variable is present and set to ``False``, then it\n+            will turn off this option.\n+\n+            For more granular control, setting a pillar variable named\n+            ``docker-ng.running.force.container_name`` will affect only the\n+            named container.\n+\n+    stop_timeout : 10\n+        If the container needs to be replaced, the container will be stopped\n+        using :py:func:`docker-ng.stop <salt.modules.dockerng.stop>`. The value\n+        of this parameter will be passed to :py:func:`docker-ng.stop\n+        <salt.modules.dockerng.stop>` as the ``timeout`` value, telling Docker\n+        how long to wait for a graceful shutdown before killing the container.\n+\n+    insecure_registry : False\n+        If ``True``, the Docker client will permit the use of insecure\n+        (non-HTTPS) registries. This only applies if the ``pull`` argument is\n+        used.\n+\n+    validate_ip_addrs : True\n+        For parameters which accept IP addresses as input, IP address\n+        validation will be performed. To disable, set this to ``False``\n+\n+    client_timeout\n+        Timeout in seconds for the Docker client. This is not a timeout for\n+        the state, but for receiving a response from the API.\n+\n+        .. note::\n+\n+            This is only used if Salt needs to pull the requested image.\n+\n+\n+    **CONTAINER CONFIGURATION PARAMETERS**\n+\n+    command\n+        Command to run in the container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - command: bash\n+\n+    hostname\n+        Hostname of the container. If not provided, and if a ``name`` has been\n+        provided, the ``hostname`` will default to the ``name`` that was\n+        passed.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - hostname: web1\n+\n+        .. warning::\n+\n+            ``hostname`` cannot be set if ``network_mode`` is set to ``host``.\n+            The below example will result in an error:\n+\n+            .. code-block:: yaml\n+\n+                foo:\n+                  docker-ng.running:\n+                    - pull: bar/baz:latest\n+                    - hostname: web1\n+                    - network_mode: host\n+\n+    domainname\n+        Domain name of the container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - hostname: domain.tld\n+\n+\n+    interactive : False\n+        Leave stdin open\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - interactive: True\n+\n+    tty : False\n+        Attach TTYs\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - tty: True\n+\n+    detach : True\n+        If ``True``, run ``command`` in the background (daemon mode)\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - detach: False\n+\n+    user\n+        User under which to run docker\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - user: foo\n+\n+    memory : 0\n+        Memory limit. Can be specified in bytes or using single-letter units\n+        (i.e. ``512M``, ``2G``, etc.). A value of ``0`` (the default) means no\n+        memory limit.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - memory: 512M\n+\n+    memory_swap : -1\n+        Total memory limit (memory plus swap). Set to ``-1`` to disable swap. A\n+        value of ``0`` means no swap limit.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - memory_swap: 1G\n+\n+    mac_address\n+        MAC address to use for the container. If not specified, a random MAC\n+        address will be used.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - mac_address: 01:23:45:67:89:0a\n+\n+    network_disabled : False\n+        If ``True``, networking will be disabled within the container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - network_disabled: True\n+\n+    working_dir\n+        Working directory inside the container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - working_dir: /var/log/nginx\n+\n+    entrypoint\n+        Entrypoint for the container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - entrypoint: \"mycmd --arg1 --arg2\"\n+\n+        The entrypoint can also be specified as a list of arguments:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - entrypoint:\n+                  - mycmd\n+                  - --arg1\n+                  - --arg2\n+\n+    environment\n+        Either a list of variable/value mappings, or a list of strings in the\n+        format ``VARNAME=value``. The below two examples are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - environment:\n+                  - VAR1: value\n+                  - VAR2: value\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - environment:\n+                  - VAR1=value\n+                  - VAR2=value\n+\n+    ports\n+        A list of ports to expose on the container. Can either be a\n+        comma-separated list or a YAML list. If the protocol is omitted, the\n+        port will be assumed to be a TCP port. The below two examples are\n+        equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - ports: 1111,2222/udp\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - ports:\n+                  - 1111\n+                  - 2222/udp\n+\n+    volumes : None\n+        List of directories to expose as volumes. Can either be a\n+        comma-separated list or a YAML list. The below two examples are\n+        equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - volumes: /mnt/vol1,/mnt/vol2\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - volumes:\n+                  - /mnt/vol1\n+                  - /mnt/vol2\n+\n+    cpu_shares\n+        CPU shares (relative weight)\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cpu_shares: 0.5\n+\n+    cpuset\n+        CPUs on which which to allow execution, specified as a string\n+        containing a range (e.g. ``0-3``) or a comma-separated list of CPUs\n+        (e.g. ``0,1``).\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cpuset: \"0,1\"\n+\n+    binds\n+        Files/directories to bind mount. Each bind mount should be passed in\n+        the format ``<host_path>:<container_path>:<read_only>``, where\n+        ``<read_only>`` is one of ``rw`` (for read-write access) or ``ro`` (for\n+        read-only access).\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - binds: /srv/www:/var/www:ro,/etc/foo.conf:/usr/local/etc/foo.conf:rw\n+\n+        Binds can be passed as a YAML list instead of a comma-separated list:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - binds:\n+                  - /srv/www:/var/www:ro\n+                  - /home/myuser/conf/foo.conf:/etc/foo.conf:rw\n+\n+        Optionally, the read-only information can be left off the end and the\n+        bind mount will be assumed to be read-write. The example below is\n+        equivalent to the one above:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - binds:\n+                  - /srv/www:/var/www:ro\n+                  - /home/myuser/conf/foo.conf:/etc/foo.conf\n+\n+    port_bindings\n+        Bind exposed ports. Port bindings should be passed in the same way as\n+        the ``--publish`` argument to the ``docker run`` CLI command:\n+\n+        - ``ip:hostPort:containerPort`` - Bind a specific IP and port on the\n+          host to a specific port within the container.\n+        - ``ip::containerPort`` - Bind a specific IP and an ephemeral port to a\n+          specific port within the container.\n+        - ``hostPort:containerPort`` - Bind a specific port on all of the\n+          host's interfaces to a specific port within the container.\n+        - ``containerPort`` - Bind an ephemeral port on all of the host's\n+          interfaces to a specific port within the container.\n+\n+        Multiple bindings can be separated by commas, or passed as a Python\n+        list. The below two examples are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - port_bindings: \"5000:5000,2123:2123/udp,8080\"\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - port_bindings:\n+                  - 5000:5000\n+                  - 2123:2123/udp\n+                  - 8080\n+\n+        .. note::\n+\n+            When configuring bindings for UDP ports, the protocol must be\n+            passed in the ``containerPort`` value, as seen in the examples\n+            above.\n+\n+    lxc_conf\n+        Additional LXC configuration parameters to set before starting the\n+        container.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - lxc_conf:\n+                  - lxc.utsname: docker\n+\n+        .. note::\n+\n+            These LXC configuration parameters will only have the desired\n+            effect if the container is using the LXC execution driver, which\n+            has not been the default for some time.\n+\n+    publish_all_ports : False\n+        Allocates a random host port for each port exposed using the ``ports``\n+        parameter\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - ports: 8080\n+                - publish_all_ports: True\n+\n+    links\n+        Link this container to another. Links should be specified in the format\n+        ``<container_name_or_id>:<link_alias>``. Multiple links can be passed,\n+        either as a comma separated list or a YAML list. The below two examples\n+        are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - links: web1:link1,web2:link2\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - links:\n+                  - web1:link1\n+                  - web2:link2\n+\n+    dns\n+        List of DNS nameservers. Can be passed as a comma-separated list or a\n+        YAML list. The below two examples are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - dns: 8.8.8.8,8.8.4.4\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - dns:\n+                  - 8.8.8.8\n+                  - 8.8.4.4\n+\n+        .. note::\n+\n+            To skip IP address validation, use ``validate_ip_addrs=False``\n+\n+    dns_search\n+        List of DNS search domains. Can be passed as a comma-separated list\n+        or a YAML list. The below two examples are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - dns_search: foo1.domain.tld,foo2.domain.tld\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - dns_search:\n+                  - foo1.domain.tld\n+                  - foo2.domain.tld\n+\n+    volumes_from\n+        Container names or IDs from which the container will get volumes. Can\n+        be passed as a comma-separated list or a YAML list. The below two\n+        examples are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - volumes_from: foo\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - volumes_from:\n+                  - foo\n+\n+    network_mode : bridge\n+        One of the following:\n+\n+        - ``bridge`` - Creates a new network stack for the container on the\n+          docker bridge\n+        - ``null`` - No networking (equivalent of the Docker CLI argument\n+          ``--net=none``)\n+        - ``container:<name_or_id>`` - Reuses another container's network stack\n+        - ``host`` - Use the host's network stack inside the container\n+\n+          .. warning::\n+\n+                Using ``host`` mode gives the container full access to the\n+                hosts system's services (such as D-bus), and is therefore\n+                considered insecure.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - network_mode: null\n+\n+    restart_policy\n+        Set a restart policy for the container. Must be passed as a string in\n+        the format ``policy[:retry_count]`` where ``policy`` is one of\n+        ``always`` or ``on-failure``, and ``retry_count`` is an optional limit\n+        to the number of retries. The retry count is ignored when using the\n+        ``always`` restart policy.\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - restart_policy: on-failure:5\n+\n+            bar:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - restart_policy: always\n+\n+    cap_add\n+        List of capabilities to add within the container. Can be passed as a\n+        comma-separated list or a Python list. The below two examples are\n+        equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cap_add: SYS_ADMIN,MKNOD\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cap_add:\n+                  - SYS_ADMIN\n+                  - MKNOD\n+\n+        .. note::\n+\n+            This option requires Docker 1.2.0 or newer.\n+\n+    cap_drop\n+        List of capabilities to drop within the container. Can be passed as a\n+        comma-separated list or a Python list. The below two examples are\n+        equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cap_drop: SYS_ADMIN,MKNOD\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - cap_drop:\n+                  - SYS_ADMIN\n+                  - MKNOD\n+\n+        .. note::\n+\n+            This option requires Docker 1.2.0 or newer.\n+\n+    extra_hosts\n+        Additional hosts to add to the container's /etc/hosts file. Can be\n+        passed as a comma-separated list or a Python list. The below two\n+        exampels are equivalent:\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - extra_hosts: web1:10.9.8.7,web2:10.9.8.8\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - extra_hosts:\n+                  - web1:10.9.8.7\n+                  - web2:10.9.8.8\n+\n+        .. note::\n+\n+            To skip IP address validation, use ``validate_ip_addrs=False``\n+\n+        .. note::\n+\n+            This option requires Docker 1.3.0 or newer.\n+\n+    pid_mode\n+        Set to ``host`` to use the host container's PID namespace within the\n+        container\n+\n+        .. code-block:: yaml\n+\n+            foo:\n+              docker-ng.running:\n+                - pull: bar/baz:latest\n+                - pid_mode: host\n+\n+        .. note::\n+\n+            This option requires Docker 1.5.0 or newer.\n+    '''\n+    ret = {'name': name,\n+           'changes': {},\n+           'result': False,\n+           'comment': ''}\n+    num_of_image_args = len([x for x in (pull, build, load) if x is not None])\n+    if num_of_image_args == 0:\n+        ret['comment'] = ('Missing required argument. One of \\'pull\\', '\n+                          '\\'build\\', or \\'load\\' is required.')\n+        return ret\n+    elif num_of_image_args > 1:\n+        ret['comment'] = ('Only one of \\'pull\\', \\'build\\', or \\'load\\' is '\n+                          'permitted.')\n+        return ret\n+\n+    if name not in __salt__['docker-ng.list_containers'](all=True):\n+        pre_config = {}\n+    else:\n+        try:\n+            pre_config = __salt__['docker-ng.inspect_container'](name)\n+            if 'Config' in pre_config and 'Image' in pre_config['Config']:\n+                current_image = ':'.join(\n+                    _get_repo_tag(pre_config['Config']['Image'])\n+                )\n+            else:\n+                ret['comment'] = (\n+                    'Unable to detect current image for container \\'{0}\\'. '\n+                    'This might be due to a change in the Docker API.'\n+                    .format(name)\n+                )\n+                return ret\n+        except CommandExecutionError as exc:\n+            ret['comment'] = ('Error occurred checking for existence of '\n+                              'container \\'{0}\\': {1}'.format(name, exc))\n+            return ret\n+\n+    if pull:\n+        action = 'pulled'\n+        # Ensure that we have repo:tag notation\n+        desired_image = ':'.join(_get_repo_tag(pull))\n+        image_path = None\n+    else:\n+        if build:\n+            action = 'built'\n+            repacked = salt.utils.repack_dictlist(build)\n+        elif load:\n+            action = 'loaded'\n+            repacked = salt.utils.repack_dictlist(load)\n+        if len(repacked) != 1:\n+            ret['comment'] = ('Invalid configuration for \\'{0}\\'. See the '\n+                              'documentation for proper usage.'.format(action))\n+            return ret\n+        key = next(iter(repacked))\n+        image_path = repacked[key]\n+        # Ensure that we have repo:tag notation\n+        desired_image = ':'.join(_get_repo_tag(key))\n+\n+    # Find out if the image needs to be updated\n+    update_image = not pre_config or force or update or \\\n+        desired_image not in __salt__['docker-ng.list_tags']()\n+\n+    force_pillar = __pillar__.get('docker-ng.running.force.' + name, NOTSET)\n+    if force_pillar is not NOTSET:\n+        # Handles cases where pillar variable was set to 1 or 'foo'\n+        force = bool(force_pillar)\n+        log.debug(\n+            'Setting force={0} via Pillar item docker-ng.running.force.{1}'\n+            .format(force, name)\n+        )\n+    else:\n+        force_pillar = __pillar__.get('docker-ng.running.force', NOTSET)\n+        if force_pillar is not NOTSET:\n+            # Handles cases where pillar variable was set to 1 or 'foo'\n+            force = bool(force_pillar)\n+            log.debug(\n+                'Setting force={0} via Pillar item docker-ng.running.force'\n+                .format(force)\n+            )\n+\n+    # If a container is using binds, don't let it also define data-only volumes\n+    if kwargs.get('volumes') is not None and kwargs.get('binds') is not None:\n+        ret['comment'] = 'Cannot mix data-only volumes and bind mounts'\n+        return ret\n+\n+    # Don't allow conflicting options to be set\n+    if kwargs.get('publish_all_ports') \\\n+            and kwargs.get('port_bindings') is not None:\n+        ret['comment'] = 'Cannot mix publish_all_ports=True and port_bindings'\n+        return ret\n+    if kwargs.get('hostname') is not None \\\n+            and kwargs.get('network_mode') == 'host':\n+        ret['comment'] = 'Cannot mix hostname with network_mode=True'\n+        return ret\n+\n+    # Strip __pub kwargs and divide the remaining arguments into the ones for\n+    # container creation and the ones for starting containers.\n+    invalid_kwargs = []\n+    create_kwargs = {}\n+    runtime_kwargs = {}\n+    for key, val in six.iteritems(salt.utils.clean_kwargs(**kwargs)):\n+        if key in VALID_CREATE_OPTS:\n+            create_kwargs[key] = val\n+        elif key in VALID_RUNTIME_OPTS:\n+            runtime_kwargs[key] = val\n+        else:\n+            invalid_kwargs.append(key)\n+    if invalid_kwargs:\n+        ret['comment'] = (\n+            'The following arguments are invalid: {0}'\n+            .format(', '.join(invalid_kwargs))\n+        )\n+        return ret\n+\n+    # Input validation\n+    try:\n+        # Repack any dictlists that need it\n+        _prep_input(runtime_kwargs)\n+        _prep_input(create_kwargs)\n+        # Perform data type validation and, where necessary, munge\n+        # the data further so it is in a format that can be passed\n+        # to docker-ng.start.\n+        _validate_input('runtime',\n+                        runtime_kwargs,\n+                        validate_ip_addrs=validate_ip_addrs)\n+\n+        # Add any needed contiainer creation arguments based on the validated\n+        # runtime arguments.\n+        if runtime_kwargs.get('binds') is not None:\n+            # Using bind mounts requries mountpoints to be specified at the\n+            # time the container is created, so we need to add them to the\n+            # create_kwargs.\n+            create_kwargs['volumes'] = list(runtime_kwargs['binds'])\n+        if runtime_kwargs.get('port_bindings') is not None \\\n+                and create_kwargs.get('ports') is None:\n+            create_kwargs['ports'] = ','.join(\n+                [str(x) for x in runtime_kwargs['port_bindings']]\n+            )\n+\n+        # Perform data type validation and, where necessary, munge\n+        # the data further so it is in a format that can be passed\n+        # to docker-ng.create.\n+        _validate_input('create',\n+                        create_kwargs,\n+                        validate_ip_addrs=validate_ip_addrs)\n+    except SaltInvocationError as exc:\n+        ret['comment'] = '{0}'.format(exc)\n+        return ret\n+\n+    changes_needed = {}\n+    if force:\n+        # No need to check the container config if force=True, or the image was\n+        # updated in the block above.\n+        new_container = True\n+    else:\n+        # Only compare the desired configuration if the named container is\n+        # already present. If it is not, pre_config will be an empty dict,\n+        # hence \"not pre_config\" will tell us if the named container is\n+        # present.\n+        if not pre_config:\n+            new_container = True\n+        else:\n+            if current_image != desired_image:\n+                # Image name doesn't match, so there's no need to check the\n+                # container configuration.\n+                new_container = True\n+            else:\n+                # Container is the correct image, let's check the container\n+                # config and see if we need to replace the container\n+                try:\n+                    changes_needed = _compare(pre_config,\n+                                              create_kwargs,\n+                                              runtime_kwargs)\n+                    if changes_needed:\n+                        log.debug(\n+                            'docker-ng.running: Analysis of container \\'{0}\\' '\n+                            'reveals the following changes need to be made: '\n+                            '{1}'.format(name, changes_needed)\n+                        )\n+                    else:\n+                        log.debug(\n+                            'docker-ng.running: Container \\'{0}\\' already '\n+                            'matches the desired configuration'.format(name)\n+                        )\n+                except Exception as exc:\n+                    exc_info = ''.join(traceback.format_tb(sys.exc_info()[2]))\n+                    msg = (\n+                        'Uncaught exception \"{0}\" encountered while comparing '\n+                        'existing container against desired configuration.'\n+                        .format(exc)\n+                    )\n+                    log.error(msg + ' Exception info follows:\\n' + exc_info)\n+                    ret['comment'] = \\\n+                        msg + ' See minion log for exception info.'\n+                    return ret\n+                new_container = bool(changes_needed)\n+\n+    if __opts__['test']:\n+        if not new_container:\n+            ret['comment'] = (\n+                'Container \\'{0}\\' is already configured as specified'\n+                .format(name)\n+            )\n+            if update:\n+                ret['result'] = None\n+                ret['comment'] += (\n+                    ', however image \\'{0}\\' will be {1} and if this results '\n+                    'in an updated image, the container will be replaced.'\n+                    .format(desired_image, action)\n+                )\n+            else:\n+                ret['result'] = True\n+            return ret\n+        else:\n+            ret['result'] = None\n+            if not pre_config:\n+                ret['comment'] = (\n+                    'Image \\'{0}\\' will be {1}, and container \\'{2}\\' will be '\n+                    'created'.format(desired_image, action, name)\n+                )\n+            else:\n+                if update_image:\n+                    ret['comment'] = (\n+                        'Image \\'{0}\\' will be {1}, and container \\'{2}\\' '\n+                        'will be replaced'.format(desired_image, action, name)\n+                    )\n+                else:\n+                    ret['comment'] = \\\n+                        'Container \\'{0}\\' will be replaced'.format(name)\n+            return ret\n+\n+    comments = []\n+    if update_image:\n+        if pull:\n+            try:\n+                image_update = __salt__['docker-ng.pull'](\n+                    desired_image,\n+                    insecure_registry=insecure_registry\n+                )\n+            except Exception as exc:\n+                ret['comment'] = (\n+                    'Encountered error pulling {0}: {1}'\n+                    .format(desired_image, exc)\n+                )\n+                return ret\n+            # Only add to the changes dict if layers were pulled\n+            if image_update.get('Layers', {}).get('Pulled'):\n+                ret['changes']['pull'] = image_update\n+\n+        if build:\n+            try:\n+                image_update = __salt__['docker-ng.build'](\n+                    path=image_path,\n+                    image=desired_image\n+                )\n+            except Exception as exc:\n+                ret['comment'] = (\n+                    'Encountered error building {0} as {1}: {2}'\n+                    .format(image_path, desired_image, exc)\n+                )\n+                return ret\n+            if image_update['Id'] != pre_config['Id'][:12]:\n+                ret['changes']['build'] = image_update\n+\n+        if load:\n+            try:\n+                image_update = __salt__['docker-ng.load'](\n+                    path=image_path,\n+                    image=desired_image\n+                )\n+            except Exception as exc:\n+                ret['comment'] = (\n+                    'Encountered error loading {0} as {1}: {2}'\n+                    .format(image_path, desired_image, exc)\n+                )\n+                return ret\n+            if image_update.get('Layers', []):\n+                ret['changes']['load'] = image_update\n+        comments.append('Image \\'{0}\\' was {1}'.format(desired_image, action))\n+\n+    if not pre_config:\n+        pre_state = None\n+    else:\n+        pre_state = __salt__['docker-ng.state'](name)\n+\n+    if new_container:\n+        if pre_config:\n+            # Container exists, stop if necessary, then remove and recreate\n+            if pre_state != 'stopped':\n+                result = __salt__['docker-ng.stop'](name,\n+                                                    timeout=stop_timeout,\n+                                                    unpause=True)['result']\n+                if result is not True:\n+                    comments.append(\n+                        'Container was slated to be replaced, but the '\n+                        'container could not be stopped.'\n+                    )\n+                    ret['comment'] = _format_comments(comments)\n+                    return ret\n+\n+            # Remove existing container\n+            removed_ids = __salt__['docker-ng.rm'](name)\n+            if not removed_ids:\n+                comments.append('Failed to remove container {0}'.format(name))\n+                ret['comment'] = _format_comments(comments)\n+                return ret\n+\n+            # Removal was successful, add the list of removed IDs to the\n+            # changes dict.\n+            ret['changes'].setdefault('container', {})['removed'] = removed_ids\n+\n+        try:\n+            # Create new container\n+            create_result = __salt__['docker-ng.create'](\n+                desired_image,\n+                name=name,\n+                client_timeout=client_timeout,\n+                # Already validated input\n+                validate_input=False,\n+                **create_kwargs\n+            )\n+        except Exception as exc:\n+            comments.append('Failed to create new container: {0}'.format(exc))\n+            ret['comment'] = _format_comments(comments)\n+            return ret\n+\n+        # Creation of new container was successful, add the return data to the\n+        # changes dict.\n+        ret['changes'].setdefault('container', {})['added'] = create_result\n+\n+    if new_container or pre_state != 'running':\n+        try:\n+            # Start container\n+            __salt__['docker-ng.start'](\n+                name,\n+                # Already validated input earlier, no need to repeat it\n+                validate_ip_addrs=False,\n+                validate_input=False,\n+                **runtime_kwargs\n+            )\n+        except Exception as exc:\n+            comments.append(\n+                'Failed to start new container \\'{0}\\': {1}'\n+                .format(name, exc)\n+            )\n+            ret['comment'] = _format_comments(comments)\n+            return ret\n+\n+        time.sleep(2)","path":"salt/states/dockerng.py","position":1403,"original_position":1403,"commit_id":"8cb4db223cdc96a31313e05a9c6d3491ffecbaae","original_commit_id":"8cb4db223cdc96a31313e05a9c6d3491ffecbaae","user":{"login":"terminalmage","id":328598,"avatar_url":"https://avatars.githubusercontent.com/u/328598?v=3","gravatar_id":"","url":"https://api.github.com/users/terminalmage","html_url":"https://github.com/terminalmage","followers_url":"https://api.github.com/users/terminalmage/followers","following_url":"https://api.github.com/users/terminalmage/following{/other_user}","gists_url":"https://api.github.com/users/terminalmage/gists{/gist_id}","starred_url":"https://api.github.com/users/terminalmage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terminalmage/subscriptions","organizations_url":"https://api.github.com/users/terminalmage/orgs","repos_url":"https://api.github.com/users/terminalmage/repos","events_url":"https://api.github.com/users/terminalmage/events{/privacy}","received_events_url":"https://api.github.com/users/terminalmage/received_events","type":"User","site_admin":false},"body":"Good catch! That was in there for testing purposes when I was developing, and I forgot to remove it. I'll get it when I make the changes I mentioned [here](https://github.com/saltstack/salt/pull/22548#issuecomment-91996517).","created_at":"2015-04-12T06:13:20Z","updated_at":"2015-04-12T06:13:20Z","html_url":"https://github.com/saltstack/salt/pull/22548#discussion_r28201063","pull_request_url":"https://api.github.com/repos/saltstack/salt/pulls/22548","_links":{"self":{"href":"https://api.github.com/repos/saltstack/salt/pulls/comments/28201063"},"html":{"href":"https://github.com/saltstack/salt/pull/22548#discussion_r28201063"},"pull_request":{"href":"https://api.github.com/repos/saltstack/salt/pulls/22548"}}},"pull_request":{"url":"https://api.github.com/repos/saltstack/salt/pulls/22548","id":33099360,"html_url":"https://github.com/saltstack/salt/pull/22548","diff_url":"https://github.com/saltstack/salt/pull/22548.diff","patch_url":"https://github.com/saltstack/salt/pull/22548.patch","issue_url":"https://api.github.com/repos/saltstack/salt/issues/22548","number":22548,"state":"open","locked":false,"title":"Add docker-ng state module","user":{"login":"terminalmage","id":328598,"avatar_url":"https://avatars.githubusercontent.com/u/328598?v=3","gravatar_id":"","url":"https://api.github.com/users/terminalmage","html_url":"https://github.com/terminalmage","followers_url":"https://api.github.com/users/terminalmage/followers","following_url":"https://api.github.com/users/terminalmage/following{/other_user}","gists_url":"https://api.github.com/users/terminalmage/gists{/gist_id}","starred_url":"https://api.github.com/users/terminalmage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terminalmage/subscriptions","organizations_url":"https://api.github.com/users/terminalmage/orgs","repos_url":"https://api.github.com/users/terminalmage/repos","events_url":"https://api.github.com/users/terminalmage/events{/privacy}","received_events_url":"https://api.github.com/users/terminalmage/received_events","type":"User","site_admin":false},"body":"**HOLD OFF ON MERGING FOR NOW. SEE [BELOW](https://github.com/saltstack/salt/pull/22548#issuecomment-91996517) FOR MORE INFORMATION.**\r\n\r\nThis is just the first of several states that will be written.\r\n\r\n@ticosax, I'd love any feedback you're able to provide. I'll be adding to this\r\nmodule over the next week or so, please let me know what kind of states you\r\nthink would be useful.","created_at":"2015-04-11T07:23:01Z","updated_at":"2015-04-12T06:13:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"5c20654a9999a94648f801413b49644bb1f314a7","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/saltstack/salt/pulls/22548/commits","review_comments_url":"https://api.github.com/repos/saltstack/salt/pulls/22548/comments","review_comment_url":"https://api.github.com/repos/saltstack/salt/pulls/comments{/number}","comments_url":"https://api.github.com/repos/saltstack/salt/issues/22548/comments","statuses_url":"https://api.github.com/repos/saltstack/salt/statuses/8cb4db223cdc96a31313e05a9c6d3491ffecbaae","head":{"label":"terminalmage:docker-ng","ref":"docker-ng","sha":"8cb4db223cdc96a31313e05a9c6d3491ffecbaae","user":{"login":"terminalmage","id":328598,"avatar_url":"https://avatars.githubusercontent.com/u/328598?v=3","gravatar_id":"","url":"https://api.github.com/users/terminalmage","html_url":"https://github.com/terminalmage","followers_url":"https://api.github.com/users/terminalmage/followers","following_url":"https://api.github.com/users/terminalmage/following{/other_user}","gists_url":"https://api.github.com/users/terminalmage/gists{/gist_id}","starred_url":"https://api.github.com/users/terminalmage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terminalmage/subscriptions","organizations_url":"https://api.github.com/users/terminalmage/orgs","repos_url":"https://api.github.com/users/terminalmage/repos","events_url":"https://api.github.com/users/terminalmage/events{/privacy}","received_events_url":"https://api.github.com/users/terminalmage/received_events","type":"User","site_admin":false},"repo":{"id":3186460,"name":"salt","full_name":"terminalmage/salt","owner":{"login":"terminalmage","id":328598,"avatar_url":"https://avatars.githubusercontent.com/u/328598?v=3","gravatar_id":"","url":"https://api.github.com/users/terminalmage","html_url":"https://github.com/terminalmage","followers_url":"https://api.github.com/users/terminalmage/followers","following_url":"https://api.github.com/users/terminalmage/following{/other_user}","gists_url":"https://api.github.com/users/terminalmage/gists{/gist_id}","starred_url":"https://api.github.com/users/terminalmage/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terminalmage/subscriptions","organizations_url":"https://api.github.com/users/terminalmage/orgs","repos_url":"https://api.github.com/users/terminalmage/repos","events_url":"https://api.github.com/users/terminalmage/events{/privacy}","received_events_url":"https://api.github.com/users/terminalmage/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/terminalmage/salt","description":"Central system and configuration manager ","fork":true,"url":"https://api.github.com/repos/terminalmage/salt","forks_url":"https://api.github.com/repos/terminalmage/salt/forks","keys_url":"https://api.github.com/repos/terminalmage/salt/keys{/key_id}","collaborators_url":"https://api.github.com/repos/terminalmage/salt/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/terminalmage/salt/teams","hooks_url":"https://api.github.com/repos/terminalmage/salt/hooks","issue_events_url":"https://api.github.com/repos/terminalmage/salt/issues/events{/number}","events_url":"https://api.github.com/repos/terminalmage/salt/events","assignees_url":"https://api.github.com/repos/terminalmage/salt/assignees{/user}","branches_url":"https://api.github.com/repos/terminalmage/salt/branches{/branch}","tags_url":"https://api.github.com/repos/terminalmage/salt/tags","blobs_url":"https://api.github.com/repos/terminalmage/salt/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/terminalmage/salt/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/terminalmage/salt/git/refs{/sha}","trees_url":"https://api.github.com/repos/terminalmage/salt/git/trees{/sha}","statuses_url":"https://api.github.com/repos/terminalmage/salt/statuses/{sha}","languages_url":"https://api.github.com/repos/terminalmage/salt/languages","stargazers_url":"https://api.github.com/repos/terminalmage/salt/stargazers","contributors_url":"https://api.github.com/repos/terminalmage/salt/contributors","subscribers_url":"https://api.github.com/repos/terminalmage/salt/subscribers","subscription_url":"https://api.github.com/repos/terminalmage/salt/subscription","commits_url":"https://api.github.com/repos/terminalmage/salt/commits{/sha}","git_commits_url":"https://api.github.com/repos/terminalmage/salt/git/commits{/sha}","comments_url":"https://api.github.com/repos/terminalmage/salt/comments{/number}","issue_comment_url":"https://api.github.com/repos/terminalmage/salt/issues/comments{/number}","contents_url":"https://api.github.com/repos/terminalmage/salt/contents/{+path}","compare_url":"https://api.github.com/repos/terminalmage/salt/compare/{base}...{head}","merges_url":"https://api.github.com/repos/terminalmage/salt/merges","archive_url":"https://api.github.com/repos/terminalmage/salt/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/terminalmage/salt/downloads","issues_url":"https://api.github.com/repos/terminalmage/salt/issues{/number}","pulls_url":"https://api.github.com/repos/terminalmage/salt/pulls{/number}","milestones_url":"https://api.github.com/repos/terminalmage/salt/milestones{/number}","notifications_url":"https://api.github.com/repos/terminalmage/salt/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/terminalmage/salt/labels{/name}","releases_url":"https://api.github.com/repos/terminalmage/salt/releases{/id}","created_at":"2012-01-15T22:33:58Z","updated_at":"2015-02-28T23:03:31Z","pushed_at":"2015-04-11T07:24:58Z","git_url":"git://github.com/terminalmage/salt.git","ssh_url":"git@github.com:terminalmage/salt.git","clone_url":"https://github.com/terminalmage/salt.git","svn_url":"https://github.com/terminalmage/salt","homepage":"saltstack.org","size":97183,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":1,"default_branch":"develop"}},"base":{"label":"saltstack:develop","ref":"develop","sha":"e4d96b09228b17706c22183116a5ad8a1ad2c454","user":{"login":"saltstack","id":1147473,"avatar_url":"https://avatars.githubusercontent.com/u/1147473?v=3","gravatar_id":"","url":"https://api.github.com/users/saltstack","html_url":"https://github.com/saltstack","followers_url":"https://api.github.com/users/saltstack/followers","following_url":"https://api.github.com/users/saltstack/following{/other_user}","gists_url":"https://api.github.com/users/saltstack/gists{/gist_id}","starred_url":"https://api.github.com/users/saltstack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saltstack/subscriptions","organizations_url":"https://api.github.com/users/saltstack/orgs","repos_url":"https://api.github.com/users/saltstack/repos","events_url":"https://api.github.com/users/saltstack/events{/privacy}","received_events_url":"https://api.github.com/users/saltstack/received_events","type":"Organization","site_admin":false},"repo":{"id":1390248,"name":"salt","full_name":"saltstack/salt","owner":{"login":"saltstack","id":1147473,"avatar_url":"https://avatars.githubusercontent.com/u/1147473?v=3","gravatar_id":"","url":"https://api.github.com/users/saltstack","html_url":"https://github.com/saltstack","followers_url":"https://api.github.com/users/saltstack/followers","following_url":"https://api.github.com/users/saltstack/following{/other_user}","gists_url":"https://api.github.com/users/saltstack/gists{/gist_id}","starred_url":"https://api.github.com/users/saltstack/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/saltstack/subscriptions","organizations_url":"https://api.github.com/users/saltstack/orgs","repos_url":"https://api.github.com/users/saltstack/repos","events_url":"https://api.github.com/users/saltstack/events{/privacy}","received_events_url":"https://api.github.com/users/saltstack/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/saltstack/salt","description":"Infrastructure automation and management system","fork":false,"url":"https://api.github.com/repos/saltstack/salt","forks_url":"https://api.github.com/repos/saltstack/salt/forks","keys_url":"https://api.github.com/repos/saltstack/salt/keys{/key_id}","collaborators_url":"https://api.github.com/repos/saltstack/salt/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/saltstack/salt/teams","hooks_url":"https://api.github.com/repos/saltstack/salt/hooks","issue_events_url":"https://api.github.com/repos/saltstack/salt/issues/events{/number}","events_url":"https://api.github.com/repos/saltstack/salt/events","assignees_url":"https://api.github.com/repos/saltstack/salt/assignees{/user}","branches_url":"https://api.github.com/repos/saltstack/salt/branches{/branch}","tags_url":"https://api.github.com/repos/saltstack/salt/tags","blobs_url":"https://api.github.com/repos/saltstack/salt/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/saltstack/salt/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/saltstack/salt/git/refs{/sha}","trees_url":"https://api.github.com/repos/saltstack/salt/git/trees{/sha}","statuses_url":"https://api.github.com/repos/saltstack/salt/statuses/{sha}","languages_url":"https://api.github.com/repos/saltstack/salt/languages","stargazers_url":"https://api.github.com/repos/saltstack/salt/stargazers","contributors_url":"https://api.github.com/repos/saltstack/salt/contributors","subscribers_url":"https://api.github.com/repos/saltstack/salt/subscribers","subscription_url":"https://api.github.com/repos/saltstack/salt/subscription","commits_url":"https://api.github.com/repos/saltstack/salt/commits{/sha}","git_commits_url":"https://api.github.com/repos/saltstack/salt/git/commits{/sha}","comments_url":"https://api.github.com/repos/saltstack/salt/comments{/number}","issue_comment_url":"https://api.github.com/repos/saltstack/salt/issues/comments{/number}","contents_url":"https://api.github.com/repos/saltstack/salt/contents/{+path}","compare_url":"https://api.github.com/repos/saltstack/salt/compare/{base}...{head}","merges_url":"https://api.github.com/repos/saltstack/salt/merges","archive_url":"https://api.github.com/repos/saltstack/salt/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/saltstack/salt/downloads","issues_url":"https://api.github.com/repos/saltstack/salt/issues{/number}","pulls_url":"https://api.github.com/repos/saltstack/salt/pulls{/number}","milestones_url":"https://api.github.com/repos/saltstack/salt/milestones{/number}","notifications_url":"https://api.github.com/repos/saltstack/salt/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/saltstack/salt/labels{/name}","releases_url":"https://api.github.com/repos/saltstack/salt/releases{/id}","created_at":"2011-02-20T20:16:56Z","updated_at":"2015-04-12T00:31:07Z","pushed_at":"2015-04-12T03:55:48Z","git_url":"git://github.com/saltstack/salt.git","ssh_url":"git@github.com:saltstack/salt.git","clone_url":"https://github.com/saltstack/salt.git","svn_url":"https://github.com/saltstack/salt","homepage":"saltstack.com","size":207585,"stargazers_count":4973,"watchers_count":4973,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2068,"mirror_url":null,"open_issues_count":2455,"forks":2068,"open_issues":2455,"watchers":4973,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/saltstack/salt/pulls/22548"},"html":{"href":"https://github.com/saltstack/salt/pull/22548"},"issue":{"href":"https://api.github.com/repos/saltstack/salt/issues/22548"},"comments":{"href":"https://api.github.com/repos/saltstack/salt/issues/22548/comments"},"review_comments":{"href":"https://api.github.com/repos/saltstack/salt/pulls/22548/comments"},"review_comment":{"href":"https://api.github.com/repos/saltstack/salt/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/saltstack/salt/pulls/22548/commits"},"statuses":{"href":"https://api.github.com/repos/saltstack/salt/statuses/8cb4db223cdc96a31313e05a9c6d3491ffecbaae"}}}}