浏览代码

Merge pull request #6368 from xificurC/master

adds --no-interpolate to docker-compose config
Ian Campbell 6 年之前
父节点
当前提交
615c01c50a
共有 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')
     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',

+ 6 - 4
compose/cli/main.py

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

+ 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')
     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):
-    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,
         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)
-        processed_config['networks'] = interpolate_config_section(
+            environment,
+            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

+ 14 - 6
compose/config/serialize.py

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

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

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