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( networks = load_mapping(
config_details.config_files, 'get_networks', 'Network' config_details.config_files, 'get_networks', 'Network'
) )
secrets = load_mapping( secrets = load_secrets(config_details.config_files, config_details.working_dir)
config_details.config_files, 'get_secrets', 'Secrets')
service_dicts = load_services(config_details, main_file) service_dicts = load_services(config_details, main_file)
if main_file.version != V1: if main_file.version != V1:
@ -364,22 +363,12 @@ def load_mapping(config_files, get_func, entity_type):
external = config.get('external') external = config.get('external')
if external: if external:
if len(config.keys()) > 1: validate_external(entity_type, name, config)
raise ConfigurationError(
'{} {} declared as external but specifies'
' additional attributes ({}). '.format(
entity_type,
name,
', '.join([k for k in config.keys() if k != 'external'])
)
)
if isinstance(external, dict): if isinstance(external, dict):
config['external_name'] = external.get('name') config['external_name'] = external.get('name')
else: else:
config['external_name'] = name config['external_name'] = name
mapping[name] = config
if 'driver_opts' in config: if 'driver_opts' in config:
config['driver_opts'] = build_string_dict( config['driver_opts'] = build_string_dict(
config['driver_opts'] config['driver_opts']
@ -391,6 +380,39 @@ def load_mapping(config_files, get_func, entity_type):
return mapping 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 load_services(config_details, config_file):
def build_service(service_name, service_dict, service_names): def build_service(service_name, service_dict, service_names):
service_config = ServiceConfig.with_abs_paths( 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_VOLUME = 'com.docker.compose.volume'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
SECRETS_PATH = '/run/secrets'
COMPOSEFILE_V1 = '1' COMPOSEFILE_V1 = '1'
COMPOSEFILE_V2_0 = '2.0' COMPOSEFILE_V2_0 = '2.0'
COMPOSEFILE_V2_1 = '2.1' COMPOSEFILE_V2_1 = '2.1'

View File

@ -104,6 +104,11 @@ class Project(object):
for volume_spec in service_dict.get('volumes', []) 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( project.services.append(
Service( Service(
service_dict.pop('name'), service_dict.pop('name'),
@ -114,6 +119,7 @@ class Project(object):
links=links, links=links,
network_mode=network_mode, network_mode=network_mode,
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets,
**service_dict) **service_dict)
) )
@ -553,6 +559,27 @@ def get_volumes_from(project, service_dict):
return [build_volume_from(vf) for vf in volumes_from] 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): def warn_for_swarm_mode(client):
info = client.info() info = client.info()
if info.get('Swarm', {}).get('LocalNodeState') == 'active': 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 docker.utils.ports import split_port
from . import __version__ from . import __version__
from . import const
from . import progress_stream from . import progress_stream
from .config import DOCKER_CONFIG_KEYS from .config import DOCKER_CONFIG_KEYS
from .config import merge_environment from .config import merge_environment
@ -139,6 +140,7 @@ class Service(object):
volumes_from=None, volumes_from=None,
network_mode=None, network_mode=None,
networks=None, networks=None,
secrets=None,
**options **options
): ):
self.name = name self.name = name
@ -149,6 +151,7 @@ class Service(object):
self.volumes_from = volumes_from or [] self.volumes_from = volumes_from or []
self.network_mode = network_mode or NetworkMode(None) self.network_mode = network_mode or NetworkMode(None)
self.networks = networks or {} self.networks = networks or {}
self.secrets = secrets or []
self.options = options self.options = options
def __repr__(self): def __repr__(self):
@ -692,9 +695,14 @@ class Service(object):
override_options['binds'] = binds override_options['binds'] = binds
container_options['environment'].update(affinity) container_options['environment'].update(affinity)
if 'volumes' in container_options: container_options['volumes'] = dict(
container_options['volumes'] = dict( (v.internal, {}) for v in container_options.get('volumes') or {})
(v.internal, {}) for v in container_options['volumes'])
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 container_options['image'] = self.image_name
@ -765,6 +773,15 @@ class Service(object):
return host_config 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): def build(self, no_cache=False, pull=False, force_rm=False):
log.info('Building %s' % self.name) log.info('Building %s' % self.name)

View File

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

View File

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