mirror of https://github.com/docker/compose.git
Implement secrets using bind mounts
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
add56ce818
commit
e0c6397999
|
@ -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(
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue