From 83f35e132b37bf20baec264e49905c3ecc944ace Mon Sep 17 00:00:00 2001
From: Jonathan Giannuzzi <jonathan@giannuzzi.be>
Date: Mon, 11 Jul 2016 11:34:01 +0200
Subject: [PATCH] Add support for creating internal networks

Signed-off-by: Jonathan Giannuzzi <jonathan@giannuzzi.be>
---
 compose/config/config_schema_v2.0.json       |  3 ++-
 compose/network.py                           |  5 +++-
 docs/compose-file.md                         |  6 ++++-
 tests/acceptance/cli_test.py                 | 18 +++++++++++++
 tests/fixtures/networks/network-internal.yml | 13 ++++++++++
 tests/integration/project_test.py            | 27 ++++++++++++++++++++
 tests/unit/config/config_test.py             |  8 ++++++
 7 files changed, 77 insertions(+), 3 deletions(-)
 create mode 100755 tests/fixtures/networks/network-internal.yml

diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json
index c08fa4d7a..ac46944cc 100644
--- a/compose/config/config_schema_v2.0.json
+++ b/compose/config/config_schema_v2.0.json
@@ -246,7 +246,8 @@
             "name": {"type": "string"}
           },
           "additionalProperties": false
-        }
+        },
+        "internal": {"type": "boolean"}
       },
       "additionalProperties": false
     },
diff --git a/compose/network.py b/compose/network.py
index affba7c2d..8962a8920 100644
--- a/compose/network.py
+++ b/compose/network.py
@@ -15,7 +15,7 @@ log = logging.getLogger(__name__)
 
 class Network(object):
     def __init__(self, client, project, name, driver=None, driver_opts=None,
-                 ipam=None, external_name=None):
+                 ipam=None, external_name=None, internal=False):
         self.client = client
         self.project = project
         self.name = name
@@ -23,6 +23,7 @@ class Network(object):
         self.driver_opts = driver_opts
         self.ipam = create_ipam_config_from_dict(ipam)
         self.external_name = external_name
+        self.internal = internal
 
     def ensure(self):
         if self.external_name:
@@ -68,6 +69,7 @@ class Network(object):
                 driver=self.driver,
                 options=self.driver_opts,
                 ipam=self.ipam,
+                internal=self.internal,
             )
 
     def remove(self):
@@ -115,6 +117,7 @@ def build_networks(name, config_data, client):
             driver_opts=data.get('driver_opts'),
             ipam=data.get('ipam'),
             external_name=data.get('external_name'),
+            internal=data.get('internal'),
         )
         for network_name, data in network_config.items()
     }
diff --git a/docs/compose-file.md b/docs/compose-file.md
index f7b5a931c..59fcf3317 100644
--- a/docs/compose-file.md
+++ b/docs/compose-file.md
@@ -859,6 +859,10 @@ A full example:
             host2: 172.28.1.6
             host3: 172.28.1.7
 
+### internal
+
+By default, Docker also connects a bridge network to it to provide external connectivity. If you want to create an externally isolated overlay network, you can set this option to `true`.
+
 ### external
 
 If set to `true`, specifies that this network has been created outside of
@@ -866,7 +870,7 @@ Compose. `docker-compose up` will not attempt to create it, and will raise
 an error if it doesn't exist.
 
 `external` cannot be used in conjunction with other network configuration keys
-(`driver`, `driver_opts`, `ipam`).
+(`driver`, `driver_opts`, `ipam`, `internal`).
 
 In the example below, `proxy` is the gateway to the outside world. Instead of
 attemping to create a network called `[projectname]_outside`, Compose will
diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py
index a8fd3249d..dad23bec5 100644
--- a/tests/acceptance/cli_test.py
+++ b/tests/acceptance/cli_test.py
@@ -576,6 +576,24 @@ class CLITestCase(DockerClientTestCase):
         assert 'forward_facing' in front_aliases
         assert 'ahead' in front_aliases
 
+    @v2_only()
+    def test_up_with_network_internal(self):
+        self.require_api_version('1.23')
+        filename = 'network-internal.yml'
+        self.base_dir = 'tests/fixtures/networks'
+        self.dispatch(['-f', filename, 'up', '-d'], None)
+        internal_net = '{}_internal'.format(self.project.name)
+
+        networks = [
+            n for n in self.client.networks()
+            if n['Name'].startswith('{}_'.format(self.project.name))
+        ]
+
+        # One network was created: internal
+        assert sorted(n['Name'] for n in networks) == [internal_net]
+
+        assert networks[0]['Internal'] is True
+
     @v2_only()
     def test_up_with_network_static_addresses(self):
         filename = 'network-static-addresses.yml'
diff --git a/tests/fixtures/networks/network-internal.yml b/tests/fixtures/networks/network-internal.yml
new file mode 100755
index 000000000..1fa339b1f
--- /dev/null
+++ b/tests/fixtures/networks/network-internal.yml
@@ -0,0 +1,13 @@
+version: "2"
+
+services:
+  web:
+    image: busybox
+    command: top
+    networks:
+      - internal
+
+networks:
+  internal:
+    driver: bridge
+    internal: True
diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py
index 6e82e931f..80915c1ae 100644
--- a/tests/integration/project_test.py
+++ b/tests/integration/project_test.py
@@ -756,6 +756,33 @@ class ProjectTest(DockerClientTestCase):
         with self.assertRaises(ProjectError):
             project.up()
 
+    @v2_only()
+    def test_project_up_with_network_internal(self):
+        self.require_api_version('1.23')
+        config_data = config.Config(
+            version=V2_0,
+            services=[{
+                'name': 'web',
+                'image': 'busybox:latest',
+                'networks': {'internal': None},
+            }],
+            volumes={},
+            networks={
+                'internal': {'driver': 'bridge', 'internal': True},
+            },
+        )
+
+        project = Project.from_config(
+            client=self.client,
+            name='composetest',
+            config_data=config_data,
+        )
+        project.up()
+
+        network = self.client.networks(names=['composetest_internal'])[0]
+
+        assert network['Internal'] is True
+
     @v2_only()
     def test_project_up_volumes(self):
         vol_name = '{0:x}'.format(random.getrandbits(32))
diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py
index 1be8aefa2..d88c1d47e 100644
--- a/tests/unit/config/config_test.py
+++ b/tests/unit/config/config_test.py
@@ -101,6 +101,10 @@ class ConfigTest(unittest.TestCase):
                                 {'subnet': '172.28.0.0/16'}
                             ]
                         }
+                    },
+                    'internal': {
+                        'driver': 'bridge',
+                        'internal': True
                     }
                 }
             }, 'working_dir', 'filename.yml')
@@ -140,6 +144,10 @@ class ConfigTest(unittest.TestCase):
                         {'subnet': '172.28.0.0/16'}
                     ]
                 }
+            },
+            'internal': {
+                'driver': 'bridge',
+                'internal': True
             }
         })