Implement secrets using bind mounts

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-01-04 17:18:04 -05:00
parent add56ce818
commit e0c6397999
6 changed files with 98 additions and 17 deletions

View File

@ -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(

View File

@ -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'

View File

@ -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':

View File

@ -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)

View File

@ -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)

View File

@ -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')