mirror of https://github.com/docker/compose.git
adds --no-interpolate to docker-compose config
Signed-off-by: Peter Nagy <pnagy@gratex.com>
This commit is contained in:
parent
b09d8802ed
commit
e34d329227
|
@ -37,7 +37,7 @@ SILENT_COMMANDS = set((
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def project_from_options(project_dir, options):
|
def project_from_options(project_dir, options, additional_options={}):
|
||||||
override_dir = options.get('--project-directory')
|
override_dir = options.get('--project-directory')
|
||||||
environment_file = options.get('--env-file')
|
environment_file = options.get('--env-file')
|
||||||
environment = Environment.from_env_file(override_dir or project_dir, environment_file)
|
environment = Environment.from_env_file(override_dir or project_dir, environment_file)
|
||||||
|
@ -57,6 +57,7 @@ def project_from_options(project_dir, options):
|
||||||
environment=environment,
|
environment=environment,
|
||||||
override_dir=override_dir,
|
override_dir=override_dir,
|
||||||
compatibility=options.get('--compatibility'),
|
compatibility=options.get('--compatibility'),
|
||||||
|
interpolate=(not additional_options.get('--no-interpolate'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ def set_parallel_limit(environment):
|
||||||
parallel.GlobalLimit.set_global_limit(parallel_limit)
|
parallel.GlobalLimit.set_global_limit(parallel_limit)
|
||||||
|
|
||||||
|
|
||||||
def get_config_from_options(base_dir, options):
|
def get_config_from_options(base_dir, options, additional_options={}):
|
||||||
override_dir = options.get('--project-directory')
|
override_dir = options.get('--project-directory')
|
||||||
environment_file = options.get('--env-file')
|
environment_file = options.get('--env-file')
|
||||||
environment = Environment.from_env_file(override_dir or base_dir, environment_file)
|
environment = Environment.from_env_file(override_dir or base_dir, environment_file)
|
||||||
|
@ -85,7 +86,8 @@ def get_config_from_options(base_dir, options):
|
||||||
)
|
)
|
||||||
return config.load(
|
return config.load(
|
||||||
config.find(base_dir, config_path, environment, override_dir),
|
config.find(base_dir, config_path, environment, override_dir),
|
||||||
options.get('--compatibility')
|
options.get('--compatibility'),
|
||||||
|
not additional_options.get('--no-interpolate')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,14 +125,14 @@ def get_client(environment, verbose=False, version=None, tls_config=None, host=N
|
||||||
|
|
||||||
def get_project(project_dir, config_path=None, project_name=None, verbose=False,
|
def get_project(project_dir, config_path=None, project_name=None, verbose=False,
|
||||||
host=None, tls_config=None, environment=None, override_dir=None,
|
host=None, tls_config=None, environment=None, override_dir=None,
|
||||||
compatibility=False):
|
compatibility=False, interpolate=True):
|
||||||
if not environment:
|
if not environment:
|
||||||
environment = Environment.from_env_file(project_dir)
|
environment = Environment.from_env_file(project_dir)
|
||||||
config_details = config.find(project_dir, config_path, environment, override_dir)
|
config_details = config.find(project_dir, config_path, environment, override_dir)
|
||||||
project_name = get_project_name(
|
project_name = get_project_name(
|
||||||
config_details.working_dir, project_name, environment
|
config_details.working_dir, project_name, environment
|
||||||
)
|
)
|
||||||
config_data = config.load(config_details, compatibility)
|
config_data = config.load(config_details, compatibility, interpolate)
|
||||||
|
|
||||||
api_version = environment.get(
|
api_version = environment.get(
|
||||||
'COMPOSE_API_VERSION',
|
'COMPOSE_API_VERSION',
|
||||||
|
|
|
@ -331,6 +331,7 @@ class TopLevelCommand(object):
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--resolve-image-digests Pin image tags to digests.
|
--resolve-image-digests Pin image tags to digests.
|
||||||
|
--no-interpolate Don't interpolate environment variables
|
||||||
-q, --quiet Only validate the configuration, don't print
|
-q, --quiet Only validate the configuration, don't print
|
||||||
anything.
|
anything.
|
||||||
--services Print the service names, one per line.
|
--services Print the service names, one per line.
|
||||||
|
@ -340,11 +341,12 @@ class TopLevelCommand(object):
|
||||||
or use the wildcard symbol to display all services
|
or use the wildcard symbol to display all services
|
||||||
"""
|
"""
|
||||||
|
|
||||||
compose_config = get_config_from_options('.', self.toplevel_options)
|
additional_options = {'--no-interpolate': options.get('--no-interpolate')}
|
||||||
|
compose_config = get_config_from_options('.', self.toplevel_options, additional_options)
|
||||||
image_digests = None
|
image_digests = None
|
||||||
|
|
||||||
if options['--resolve-image-digests']:
|
if options['--resolve-image-digests']:
|
||||||
self.project = project_from_options('.', self.toplevel_options)
|
self.project = project_from_options('.', self.toplevel_options, additional_options)
|
||||||
with errors.handle_connection_errors(self.project.client):
|
with errors.handle_connection_errors(self.project.client):
|
||||||
image_digests = image_digests_for_project(self.project)
|
image_digests = image_digests_for_project(self.project)
|
||||||
|
|
||||||
|
@ -361,14 +363,14 @@ class TopLevelCommand(object):
|
||||||
|
|
||||||
if options['--hash'] is not None:
|
if options['--hash'] is not None:
|
||||||
h = options['--hash']
|
h = options['--hash']
|
||||||
self.project = project_from_options('.', self.toplevel_options)
|
self.project = project_from_options('.', self.toplevel_options, additional_options)
|
||||||
services = [svc for svc in options['--hash'].split(',')] if h != '*' else None
|
services = [svc for svc in options['--hash'].split(',')] if h != '*' else None
|
||||||
with errors.handle_connection_errors(self.project.client):
|
with errors.handle_connection_errors(self.project.client):
|
||||||
for service in self.project.get_services(services):
|
for service in self.project.get_services(services):
|
||||||
print('{} {}'.format(service.name, service.config_hash))
|
print('{} {}'.format(service.name, service.config_hash))
|
||||||
return
|
return
|
||||||
|
|
||||||
print(serialize_config(compose_config, image_digests))
|
print(serialize_config(compose_config, image_digests, not options['--no-interpolate']))
|
||||||
|
|
||||||
def create(self, options):
|
def create(self, options):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -373,7 +373,7 @@ def check_swarm_only_config(service_dicts, compatibility=False):
|
||||||
check_swarm_only_key(service_dicts, 'configs')
|
check_swarm_only_key(service_dicts, 'configs')
|
||||||
|
|
||||||
|
|
||||||
def load(config_details, compatibility=False):
|
def load(config_details, compatibility=False, interpolate=True):
|
||||||
"""Load the configuration from a working directory and a list of
|
"""Load the configuration from a working directory and a list of
|
||||||
configuration files. Files are loaded in order, and merged on top
|
configuration files. Files are loaded in order, and merged on top
|
||||||
of each other to create the final configuration.
|
of each other to create the final configuration.
|
||||||
|
@ -383,7 +383,7 @@ def load(config_details, compatibility=False):
|
||||||
validate_config_version(config_details.config_files)
|
validate_config_version(config_details.config_files)
|
||||||
|
|
||||||
processed_files = [
|
processed_files = [
|
||||||
process_config_file(config_file, config_details.environment)
|
process_config_file(config_file, config_details.environment, interpolate=interpolate)
|
||||||
for config_file in config_details.config_files
|
for config_file in config_details.config_files
|
||||||
]
|
]
|
||||||
config_details = config_details._replace(config_files=processed_files)
|
config_details = config_details._replace(config_files=processed_files)
|
||||||
|
@ -505,7 +505,6 @@ def load_services(config_details, config_file, compatibility=False):
|
||||||
|
|
||||||
|
|
||||||
def interpolate_config_section(config_file, config, section, environment):
|
def interpolate_config_section(config_file, config, section, environment):
|
||||||
validate_config_section(config_file.filename, config, section)
|
|
||||||
return interpolate_environment_variables(
|
return interpolate_environment_variables(
|
||||||
config_file.version,
|
config_file.version,
|
||||||
config,
|
config,
|
||||||
|
@ -514,38 +513,60 @@ def interpolate_config_section(config_file, config, section, environment):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_config_file(config_file, environment, service_name=None):
|
def process_config_section(config_file, config, section, environment, interpolate):
|
||||||
services = interpolate_config_section(
|
validate_config_section(config_file.filename, config, section)
|
||||||
|
if interpolate:
|
||||||
|
return interpolate_environment_variables(
|
||||||
|
config_file.version,
|
||||||
|
config,
|
||||||
|
section,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def process_config_file(config_file, environment, service_name=None, interpolate=True):
|
||||||
|
services = process_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_service_dicts(),
|
config_file.get_service_dicts(),
|
||||||
'service',
|
'service',
|
||||||
environment)
|
environment,
|
||||||
|
interpolate,
|
||||||
|
)
|
||||||
|
|
||||||
if config_file.version > V1:
|
if config_file.version > V1:
|
||||||
processed_config = dict(config_file.config)
|
processed_config = dict(config_file.config)
|
||||||
processed_config['services'] = services
|
processed_config['services'] = services
|
||||||
processed_config['volumes'] = interpolate_config_section(
|
processed_config['volumes'] = process_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_volumes(),
|
config_file.get_volumes(),
|
||||||
'volume',
|
'volume',
|
||||||
environment)
|
environment,
|
||||||
processed_config['networks'] = interpolate_config_section(
|
interpolate,
|
||||||
|
)
|
||||||
|
processed_config['networks'] = process_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_networks(),
|
config_file.get_networks(),
|
||||||
'network',
|
'network',
|
||||||
environment)
|
environment,
|
||||||
|
interpolate,
|
||||||
|
)
|
||||||
if config_file.version >= const.COMPOSEFILE_V3_1:
|
if config_file.version >= const.COMPOSEFILE_V3_1:
|
||||||
processed_config['secrets'] = interpolate_config_section(
|
processed_config['secrets'] = process_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_secrets(),
|
config_file.get_secrets(),
|
||||||
'secret',
|
'secret',
|
||||||
environment)
|
environment,
|
||||||
|
interpolate,
|
||||||
|
)
|
||||||
if config_file.version >= const.COMPOSEFILE_V3_3:
|
if config_file.version >= const.COMPOSEFILE_V3_3:
|
||||||
processed_config['configs'] = interpolate_config_section(
|
processed_config['configs'] = process_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_configs(),
|
config_file.get_configs(),
|
||||||
'config',
|
'config',
|
||||||
environment
|
environment,
|
||||||
|
interpolate,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
processed_config = services
|
processed_config = services
|
||||||
|
|
|
@ -24,14 +24,12 @@ def serialize_dict_type(dumper, data):
|
||||||
|
|
||||||
|
|
||||||
def serialize_string(dumper, data):
|
def serialize_string(dumper, data):
|
||||||
""" Ensure boolean-like strings are quoted in the output and escape $ characters """
|
""" Ensure boolean-like strings are quoted in the output """
|
||||||
representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
|
representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
|
||||||
|
|
||||||
if isinstance(data, six.binary_type):
|
if isinstance(data, six.binary_type):
|
||||||
data = data.decode('utf-8')
|
data = data.decode('utf-8')
|
||||||
|
|
||||||
data = data.replace('$', '$$')
|
|
||||||
|
|
||||||
if data.lower() in ('y', 'n', 'yes', 'no', 'on', 'off', 'true', 'false'):
|
if data.lower() in ('y', 'n', 'yes', 'no', 'on', 'off', 'true', 'false'):
|
||||||
# Empirically only y/n appears to be an issue, but this might change
|
# Empirically only y/n appears to be an issue, but this might change
|
||||||
# depending on which PyYaml version is being used. Err on safe side.
|
# depending on which PyYaml version is being used. Err on safe side.
|
||||||
|
@ -39,6 +37,12 @@ def serialize_string(dumper, data):
|
||||||
return representer(data)
|
return representer(data)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_string_escape_dollar(dumper, data):
|
||||||
|
""" Ensure boolean-like strings are quoted in the output and escape $ characters """
|
||||||
|
data = data.replace('$', '$$')
|
||||||
|
return serialize_string(dumper, data)
|
||||||
|
|
||||||
|
|
||||||
yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
||||||
|
@ -46,8 +50,6 @@ yaml.SafeDumper.add_representer(types.SecurityOpt, serialize_config_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(str, serialize_string)
|
|
||||||
yaml.SafeDumper.add_representer(six.text_type, serialize_string)
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_config(config, image_digests=None):
|
def denormalize_config(config, image_digests=None):
|
||||||
|
@ -93,7 +95,13 @@ def v3_introduced_name_key(key):
|
||||||
return V3_5
|
return V3_5
|
||||||
|
|
||||||
|
|
||||||
def serialize_config(config, image_digests=None):
|
def serialize_config(config, image_digests=None, escape_dollar=True):
|
||||||
|
if escape_dollar:
|
||||||
|
yaml.SafeDumper.add_representer(str, serialize_string_escape_dollar)
|
||||||
|
yaml.SafeDumper.add_representer(six.text_type, serialize_string_escape_dollar)
|
||||||
|
else:
|
||||||
|
yaml.SafeDumper.add_representer(str, serialize_string)
|
||||||
|
yaml.SafeDumper.add_representer(six.text_type, serialize_string)
|
||||||
return yaml.safe_dump(
|
return yaml.safe_dump(
|
||||||
denormalize_config(config, image_digests),
|
denormalize_config(config, image_digests),
|
||||||
default_flow_style=False,
|
default_flow_style=False,
|
||||||
|
|
|
@ -613,6 +613,25 @@ class ConfigTest(unittest.TestCase):
|
||||||
excinfo.exconly()
|
excinfo.exconly()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_config_integer_service_name_raise_validation_error_v2_when_no_interpolate(self):
|
||||||
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
|
config.load(
|
||||||
|
build_config_details(
|
||||||
|
{
|
||||||
|
'version': '2',
|
||||||
|
'services': {1: {'image': 'busybox'}}
|
||||||
|
},
|
||||||
|
'working_dir',
|
||||||
|
'filename.yml'
|
||||||
|
),
|
||||||
|
interpolate=False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in
|
||||||
|
excinfo.exconly()
|
||||||
|
)
|
||||||
|
|
||||||
def test_config_integer_service_property_raise_validation_error(self):
|
def test_config_integer_service_property_raise_validation_error(self):
|
||||||
with pytest.raises(ConfigurationError) as excinfo:
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
config.load(
|
config.load(
|
||||||
|
@ -5328,6 +5347,28 @@ class SerializeTest(unittest.TestCase):
|
||||||
assert serialized_service['command'] == 'echo $$FOO'
|
assert serialized_service['command'] == 'echo $$FOO'
|
||||||
assert serialized_service['entrypoint'][0] == '$$SHELL'
|
assert serialized_service['entrypoint'][0] == '$$SHELL'
|
||||||
|
|
||||||
|
def test_serialize_escape_dont_interpolate(self):
|
||||||
|
cfg = {
|
||||||
|
'version': '2.2',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'command': 'echo $FOO',
|
||||||
|
'environment': {
|
||||||
|
'CURRENCY': '$'
|
||||||
|
},
|
||||||
|
'entrypoint': ['$SHELL', '-c'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config_dict = config.load(build_config_details(cfg), interpolate=False)
|
||||||
|
|
||||||
|
serialized_config = yaml.load(serialize_config(config_dict, escape_dollar=False))
|
||||||
|
serialized_service = serialized_config['services']['web']
|
||||||
|
assert serialized_service['environment']['CURRENCY'] == '$'
|
||||||
|
assert serialized_service['command'] == 'echo $FOO'
|
||||||
|
assert serialized_service['entrypoint'][0] == '$SHELL'
|
||||||
|
|
||||||
def test_serialize_unicode_values(self):
|
def test_serialize_unicode_values(self):
|
||||||
cfg = {
|
cfg = {
|
||||||
'version': '2.3',
|
'version': '2.3',
|
||||||
|
|
Loading…
Reference in New Issue