Bläddra i källkod

Implement secrets using bind mounts

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin 8 år sedan
förälder
incheckning
e0c6397999
6 ändrade filer med 98 tillägg och 17 borttagningar
  1. 35 13
      compose/config/config.py
  2. 2 0
      compose/const.py
  3. 27 0
      compose/project.py
  4. 20 3
      compose/service.py
  5. 2 1
      tests/unit/bundle_test.py
  6. 12 0
      tests/unit/project_test.py

+ 35 - 13
compose/config/config.py

@@ -334,8 +334,7 @@ def load(config_details):
     networks = load_mapping(
         config_details.config_files, 'get_networks', 'Network'
     )
-    secrets = load_mapping(
-        config_details.config_files, 'get_secrets', 'Secrets')
+    secrets = load_secrets(config_details.config_files, config_details.working_dir)
     service_dicts = load_services(config_details, main_file)
 
     if main_file.version != V1:
@@ -364,22 +363,12 @@ def load_mapping(config_files, get_func, entity_type):
 
             external = config.get('external')
             if external:
-                if len(config.keys()) > 1:
-                    raise ConfigurationError(
-                        '{} {} declared as external but specifies'
-                        ' additional attributes ({}). '.format(
-                            entity_type,
-                            name,
-                            ', '.join([k for k in config.keys() if k != 'external'])
-                        )
-                    )
+                validate_external(entity_type, name, config)
                 if isinstance(external, dict):
                     config['external_name'] = external.get('name')
                 else:
                     config['external_name'] = name
 
-            mapping[name] = config
-
             if 'driver_opts' in config:
                 config['driver_opts'] = build_string_dict(
                     config['driver_opts']
@@ -391,6 +380,39 @@ def load_mapping(config_files, get_func, entity_type):
     return mapping
 
 
+def validate_external(entity_type, name, config):
+    if len(config.keys()) <= 1:
+        return
+
+    raise ConfigurationError(
+        "{} {} declared as external but specifies additional attributes "
+        "({}).".format(
+            entity_type, name, ', '.join(k for k in config if k != 'external')))
+
+
+def load_secrets(config_files, working_dir):
+    mapping = {}
+
+    for config_file in config_files:
+        for name, config in config_file.get_secrets().items():
+            mapping[name] = config or {}
+            if not config:
+                continue
+
+            external = config.get('external')
+            if external:
+                validate_external('Secret', name, config)
+                if isinstance(external, dict):
+                    config['external_name'] = external.get('name')
+                else:
+                    config['external_name'] = name
+
+            if 'file' in config:
+                config['file'] = expand_path(working_dir, config['file'])
+
+    return mapping
+
+
 def load_services(config_details, config_file):
     def build_service(service_name, service_dict, service_names):
         service_config = ServiceConfig.with_abs_paths(

+ 2 - 0
compose/const.py

@@ -16,6 +16,8 @@ LABEL_VERSION = 'com.docker.compose.version'
 LABEL_VOLUME = 'com.docker.compose.volume'
 LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
 
+SECRETS_PATH = '/run/secrets'
+
 COMPOSEFILE_V1 = '1'
 COMPOSEFILE_V2_0 = '2.0'
 COMPOSEFILE_V2_1 = '2.1'

+ 27 - 0
compose/project.py

@@ -104,6 +104,11 @@ class Project(object):
                     for volume_spec in service_dict.get('volumes', [])
                 ]
 
+            secrets = get_secrets(
+                service_dict['name'],
+                service_dict.get('secrets') or [],
+                config_data.secrets)
+
             project.services.append(
                 Service(
                     service_dict.pop('name'),
@@ -114,6 +119,7 @@ class Project(object):
                     links=links,
                     network_mode=network_mode,
                     volumes_from=volumes_from,
+                    secrets=secrets,
                     **service_dict)
             )
 
@@ -553,6 +559,27 @@ def get_volumes_from(project, service_dict):
     return [build_volume_from(vf) for vf in volumes_from]
 
 
+def get_secrets(service, service_secrets, secret_defs):
+    secrets = []
+
+    for secret in service_secrets:
+        secret_def = secret_defs.get(secret.source)
+        if not secret_def:
+            raise ConfigurationError(
+                "Service \"{service}\" uses an undefined secret \"{secret}\" "
+                .format(service=service, secret=secret.source))
+
+        if secret_def.get('external_name'):
+            log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
+                     "External secrets are not available to containers created by "
+                     "docker-compose.".format(service=service, secret=secret.source))
+            continue
+
+        secrets.append({'secret': secret, 'file': secret_def.get('file')})
+
+    return secrets
+
+
 def warn_for_swarm_mode(client):
     info = client.info()
     if info.get('Swarm', {}).get('LocalNodeState') == 'active':

+ 20 - 3
compose/service.py

@@ -17,6 +17,7 @@ from docker.utils.ports import build_port_bindings
 from docker.utils.ports import split_port
 
 from . import __version__
+from . import const
 from . import progress_stream
 from .config import DOCKER_CONFIG_KEYS
 from .config import merge_environment
@@ -139,6 +140,7 @@ class Service(object):
         volumes_from=None,
         network_mode=None,
         networks=None,
+        secrets=None,
         **options
     ):
         self.name = name
@@ -149,6 +151,7 @@ class Service(object):
         self.volumes_from = volumes_from or []
         self.network_mode = network_mode or NetworkMode(None)
         self.networks = networks or {}
+        self.secrets = secrets or []
         self.options = options
 
     def __repr__(self):
@@ -692,9 +695,14 @@ class Service(object):
         override_options['binds'] = binds
         container_options['environment'].update(affinity)
 
-        if 'volumes' in container_options:
-            container_options['volumes'] = dict(
-                (v.internal, {}) for v in container_options['volumes'])
+        container_options['volumes'] = dict(
+            (v.internal, {}) for v in container_options.get('volumes') or {})
+
+        secret_volumes = self.get_secret_volumes()
+        if secret_volumes:
+            override_options['binds'].extend(v.repr() for v in secret_volumes)
+            container_options['volumes'].update(
+                (v.internal, {}) for v in secret_volumes)
 
         container_options['image'] = self.image_name
 
@@ -765,6 +773,15 @@ class Service(object):
 
         return host_config
 
+    def get_secret_volumes(self):
+        def build_spec(secret):
+            target = '{}/{}'.format(
+                const.SECRETS_PATH,
+                secret['secret'].target or secret['secret'].source)
+            return VolumeSpec(secret['file'], target, 'ro')
+
+        return [build_spec(secret) for secret in self.secrets]
+
     def build(self, no_cache=False, pull=False, force_rm=False):
         log.info('Building %s' % self.name)
 

+ 2 - 1
tests/unit/bundle_test.py

@@ -77,7 +77,8 @@ def test_to_bundle():
         version=2,
         services=services,
         volumes={'special': {}},
-        networks={'extra': {}})
+        networks={'extra': {}},
+        secrets={})
 
     with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
         output = bundle.to_bundle(config, image_digests)

+ 12 - 0
tests/unit/project_test.py

@@ -36,6 +36,7 @@ class ProjectTest(unittest.TestCase):
             ],
             networks=None,
             volumes=None,
+            secrets=None,
         )
         project = Project.from_config(
             name='composetest',
@@ -64,6 +65,7 @@ class ProjectTest(unittest.TestCase):
             ],
             networks=None,
             volumes=None,
+            secrets=None,
         )
         project = Project.from_config('composetest', config, None)
         self.assertEqual(len(project.services), 2)
@@ -170,6 +172,7 @@ class ProjectTest(unittest.TestCase):
                 }],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
@@ -202,6 +205,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
@@ -227,6 +231,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         with mock.patch.object(Service, 'containers') as mock_return:
@@ -360,6 +365,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         service = project.get_service('test')
@@ -384,6 +390,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         service = project.get_service('test')
@@ -417,6 +424,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
 
@@ -437,6 +445,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
 
@@ -457,6 +466,7 @@ class ProjectTest(unittest.TestCase):
                 ],
                 networks={'custom': {}},
                 volumes=None,
+                secrets=None,
             ),
         )
 
@@ -487,6 +497,7 @@ class ProjectTest(unittest.TestCase):
                 }],
                 networks=None,
                 volumes=None,
+                secrets=None,
             ),
         )
         self.assertEqual([c.id for c in project.containers()], ['1'])
@@ -503,6 +514,7 @@ class ProjectTest(unittest.TestCase):
                 }],
                 networks={'default': {}},
                 volumes={'data': {}},
+                secrets=None,
             ),
         )
         self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops')