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(
|
||||
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(
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue