adds --no-interpolate to docker-compose config

Signed-off-by: Peter Nagy <pnagy@gratex.com>
This commit is contained in:
Peter Nagy (NPE) 2018-11-23 11:30:49 +01:00
parent b09d8802ed
commit e34d329227
5 changed files with 103 additions and 29 deletions

View File

@ -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')
environment_file = options.get('--env-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,
override_dir=override_dir,
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)
def get_config_from_options(base_dir, options):
def get_config_from_options(base_dir, options, additional_options={}):
override_dir = options.get('--project-directory')
environment_file = options.get('--env-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(
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,
host=None, tls_config=None, environment=None, override_dir=None,
compatibility=False):
compatibility=False, interpolate=True):
if not environment:
environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment, override_dir)
project_name = get_project_name(
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(
'COMPOSE_API_VERSION',

View File

@ -331,6 +331,7 @@ class TopLevelCommand(object):
Options:
--resolve-image-digests Pin image tags to digests.
--no-interpolate Don't interpolate environment variables
-q, --quiet Only validate the configuration, don't print
anything.
--services Print the service names, one per line.
@ -340,11 +341,12 @@ class TopLevelCommand(object):
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
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):
image_digests = image_digests_for_project(self.project)
@ -361,14 +363,14 @@ class TopLevelCommand(object):
if options['--hash'] is not None:
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
with errors.handle_connection_errors(self.project.client):
for service in self.project.get_services(services):
print('{} {}'.format(service.name, service.config_hash))
return
print(serialize_config(compose_config, image_digests))
print(serialize_config(compose_config, image_digests, not options['--no-interpolate']))
def create(self, options):
"""

View File

@ -373,7 +373,7 @@ def check_swarm_only_config(service_dicts, compatibility=False):
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
configuration files. Files are loaded in order, and merged on top
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)
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
]
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):
validate_config_section(config_file.filename, config, section)
return interpolate_environment_variables(
config_file.version,
config,
@ -514,38 +513,60 @@ def interpolate_config_section(config_file, config, section, environment):
)
def process_config_file(config_file, environment, service_name=None):
services = interpolate_config_section(
def process_config_section(config_file, config, section, environment, interpolate):
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.get_service_dicts(),
'service',
environment)
environment,
interpolate,
)
if config_file.version > V1:
processed_config = dict(config_file.config)
processed_config['services'] = services
processed_config['volumes'] = interpolate_config_section(
processed_config['volumes'] = process_config_section(
config_file,
config_file.get_volumes(),
'volume',
environment)
processed_config['networks'] = interpolate_config_section(
environment,
interpolate,
)
processed_config['networks'] = process_config_section(
config_file,
config_file.get_networks(),
'network',
environment)
environment,
interpolate,
)
if config_file.version >= const.COMPOSEFILE_V3_1:
processed_config['secrets'] = interpolate_config_section(
processed_config['secrets'] = process_config_section(
config_file,
config_file.get_secrets(),
'secret',
environment)
environment,
interpolate,
)
if config_file.version >= const.COMPOSEFILE_V3_3:
processed_config['configs'] = interpolate_config_section(
processed_config['configs'] = process_config_section(
config_file,
config_file.get_configs(),
'config',
environment
environment,
interpolate,
)
else:
processed_config = services

View File

@ -24,14 +24,12 @@ def serialize_dict_type(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
if isinstance(data, six.binary_type):
data = data.decode('utf-8')
data = data.replace('$', '$$')
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
# depending on which PyYaml version is being used. Err on safe side.
@ -39,6 +37,12 @@ def serialize_string(dumper, 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.VolumeFromSpec, 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.ServiceConfig, 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):
@ -93,7 +95,13 @@ def v3_introduced_name_key(key):
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(
denormalize_config(config, image_digests),
default_flow_style=False,

View File

@ -613,6 +613,25 @@ class ConfigTest(unittest.TestCase):
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):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
@ -5328,6 +5347,28 @@ class SerializeTest(unittest.TestCase):
assert serialized_service['command'] == 'echo $$FOO'
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):
cfg = {
'version': '2.3',