Преглед на файлове

adds --no-interpolate to docker-compose config

Signed-off-by: Peter Nagy <[email protected]>
Peter Nagy (NPE) преди 7 години
родител
ревизия
e34d329227
променени са 5 файла, в които са добавени 103 реда и са изтрити 29 реда
  1. 7 5
      compose/cli/command.py
  2. 6 4
      compose/cli/main.py
  3. 35 14
      compose/config/config.py
  4. 14 6
      compose/config/serialize.py
  5. 41 0
      tests/unit/config/config_test.py

+ 7 - 5
compose/cli/command.py

@@ -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',

+ 6 - 4
compose/cli/main.py

@@ -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):
         """

+ 35 - 14
compose/config/config.py

@@ -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

+ 14 - 6
compose/config/serialize.py

@@ -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,

+ 41 - 0
tests/unit/config/config_test.py

@@ -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',