diff --git a/esrally/config.py b/esrally/config.py
index a78ebf77..b8e0d79d 100644
--- a/esrally/config.py
+++ b/esrally/config.py
@@ -447,6 +447,7 @@ class ConfigFactory:
             # the Elasticsearch directory is just the last path component (relative to the source root directory)
             config["source"]["elasticsearch.src.subdir"] = io.basename(source_dir)
 
+        if gradle_bin:
             config["build"] = {}
             config["build"]["gradle.bin"] = gradle_bin
 
diff --git a/esrally/track/loader.py b/esrally/track/loader.py
index 513b1e4f..d04de55e 100644
--- a/esrally/track/loader.py
+++ b/esrally/track/loader.py
@@ -757,9 +757,10 @@ class TrackSpecificationReader:
     Creates a track instances based on its parsed JSON description.
     """
 
-    def __init__(self, override_auto_manage_indices=None, source=io.FileSource):
+    def __init__(self, override_auto_manage_indices=None, track_params=None, source=io.FileSource):
         self.name = None
         self.override_auto_manage_indices = override_auto_manage_indices
+        self.track_params = track_params if track_params else {}
         self.source = source
         self.index_op_type_warning_issued = False
 
@@ -770,7 +771,7 @@ class TrackSpecificationReader:
         meta_data = self._r(track_specification, "meta", mandatory=False)
         indices = [self._create_index(idx, mapping_dir)
                    for idx in self._r(track_specification, "indices", mandatory=False, default_value=[])]
-        templates = [self._create_template(tpl, mapping_dir)
+        templates = [self._create_index_template(tpl, mapping_dir)
                      for tpl in self._r(track_specification, "templates", mandatory=False, default_value=[])]
         corpora = self._create_corpora(self._r(track_specification, "corpora", mandatory=False, default_value=[]), indices)
         # TODO: Remove this in Rally 0.10.0
@@ -816,7 +817,7 @@ class TrackSpecificationReader:
         body_file = self._r(index_spec, "body", mandatory=False)
         if body_file:
             with self.source(os.path.join(mapping_dir, body_file), "rt") as f:
-                body = json.load(f)
+                body = self._load_template(f.read(), index_name)
         else:
             body = None
 
@@ -832,15 +833,25 @@ class TrackSpecificationReader:
 
         return track.Index(name=index_name, body=body, auto_managed=auto_managed, types=types)
 
-    def _create_template(self, tpl_spec, mapping_dir):
+    def _create_index_template(self, tpl_spec, mapping_dir):
         name = self._r(tpl_spec, "name")
         index_pattern = self._r(tpl_spec, "index-pattern")
         delete_matching_indices = self._r(tpl_spec, "delete-matching-indices", mandatory=False, default_value=True)
         template_file = os.path.join(mapping_dir, self._r(tpl_spec, "template"))
         with self.source(template_file, "rt") as f:
-            template_content = json.load(f)
+            template_content = self._load_template(f.read(), name)
         return track.IndexTemplate(name, index_pattern, template_content, delete_matching_indices)
 
+    def _load_template(self, contents, description):
+        try:
+            rendered = render_template(loader=jinja2.DictLoader({"default": contents}),
+                                       template_name="default",
+                                       template_vars=self.track_params)
+            return json.loads(rendered)
+        except (json.JSONDecodeError, jinja2.exceptions.TemplateError) as e:
+            logger.exception("Could not load file template for %s." % description)
+            raise TrackSyntaxError("Could not load file template for '%s'" % description, str(e))
+
     def _create_corpora(self, corpora_specs, indices):
         document_corpora = []
         known_corpora_names = set()

Allow to use track parameters also in index / template definitions
Currently index definitions are read as is. However, it can be useful to allow track parameters also for index definition files. For consistency, we should allow the same for index templates.

Technically, this means that we will treat index definition files also as Jinja templates.
diff --git a/tests/track/loader_test.py b/tests/track/loader_test.py
index 4f9e524e..ea6b4996 100644
--- a/tests/track/loader_test.py
+++ b/tests/track/loader_test.py
@@ -1426,8 +1426,20 @@ class TrackSpecificationReaderTests(TestCase):
                 }
             ]
         }
-        reader = loader.TrackSpecificationReader(source=io.DictStringFileSourceFactory({
-            "/mappings/body.json": ['{"mappings": {"main": "empty-for-test", "secondary": "empty-for-test"}}']
+        reader = loader.TrackSpecificationReader(
+            track_params={"number_of_shards": 3},
+            source=io.DictStringFileSourceFactory({
+            "/mappings/body.json": ["""
+            {
+                "settings": {
+                    "number_of_shards": {{ number_of_shards }}
+                },            
+                "mappings": {
+                    "main": "empty-for-test", 
+                    "secondary": "empty-for-test"
+                }
+            }
+            """]
         }))
         resulting_track = reader("unittest", track_specification, "/mappings")
         self.assertEqual("unittest", resulting_track.name)
@@ -1435,7 +1447,16 @@ class TrackSpecificationReaderTests(TestCase):
         # indices
         self.assertEqual(1, len(resulting_track.indices))
         self.assertEqual("index-historical", resulting_track.indices[0].name)
-        self.assertDictEqual({"mappings": {"main": "empty-for-test", "secondary": "empty-for-test"}}, resulting_track.indices[0].body)
+        self.assertDictEqual({
+            "settings": {
+                "number_of_shards": 3
+            },
+            "mappings":
+                {
+                    "main": "empty-for-test",
+                    "secondary": "empty-for-test"
+                }
+        }, resulting_track.indices[0].body)
         self.assertEqual(2, len(resulting_track.indices[0].types))
         self.assertEqual("main", resulting_track.indices[0].types[0].name)
         self.assertEqual("secondary", resulting_track.indices[0].types[1].name)
@@ -1492,8 +1513,17 @@ class TrackSpecificationReaderTests(TestCase):
             "operations": [],
             "challenges": []
         }
-        reader = loader.TrackSpecificationReader(source=io.DictStringFileSourceFactory({
-            "/mappings/default-template.json": ['{"some-index-template": "empty-for-test"}'],
+        reader = loader.TrackSpecificationReader(
+            track_params={"index_pattern": "*"},
+            source=io.DictStringFileSourceFactory({
+                "/mappings/default-template.json": ["""
+                {
+                    "index_patterns": [ "{{index_pattern}}"],
+                    "settings": {
+                        "number_of_shards": {{ number_of_shards | default(1) }} 
+                    }                    
+                }
+                """],
         }))
         resulting_track = reader("unittest", track_specification, "/mappings")
         self.assertEqual("unittest", resulting_track.name)
@@ -1502,7 +1532,13 @@ class TrackSpecificationReaderTests(TestCase):
         self.assertEqual(1, len(resulting_track.templates))
         self.assertEqual("my-index-template", resulting_track.templates[0].name)
         self.assertEqual("*", resulting_track.templates[0].pattern)
-        self.assertEqual({"some-index-template": "empty-for-test"}, resulting_track.templates[0].content)
+        self.assertDictEqual(
+            {
+                "index_patterns": ["*"],
+                "settings": {
+                    "number_of_shards": 1
+                }
+            }, resulting_track.templates[0].content)
         self.assertEqual(0, len(resulting_track.challenges))
 
     def test_types_are_optional_for_user_managed_indices(self):

pip install -e .
parse_log_pytest
pytest
pytest pytest-benchmark
apt-get update apt-get install -y gcc python3-pip python3-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
elasticsearch==6.0.0
-e git+https://github.com/elastic/rally.git@188495b1fdcd8f9e36625ca2b10f1c9659750290#egg=esrally
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==2.9.5
jsonschema==2.5.1
MarkupSafe==2.0.1
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
psutil==5.4.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
py-cpuinfo==3.2.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-benchmark==3.4.1
tabulate==0.8.1
thespian==3.9.2
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.22
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: rally
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - elasticsearch==6.0.0
      - jinja2==2.9.5
      - jsonschema==2.5.1
      - markupsafe==2.0.1
      - psutil==5.4.0
      - py-cpuinfo==3.2.0
      - pytest-benchmark==3.4.1
      - tabulate==0.8.1
      - thespian==3.9.2
      - urllib3==1.22
prefix: /opt/conda/envs/rally

tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_valid_track_specification tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_valid_track_specification_with_index_template
tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_directory tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_directory_without_track tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_file tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_file_but_not_json tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_named_pipe tests/track/loader_test.py::SimpleTrackRepositoryTests::test_track_from_non_existing_path tests/track/loader_test.py::GitRepositoryTests::test_track_from_existing_repo tests/track/loader_test.py::TrackPreparationTests::test_decompresses_if_archive_available tests/track/loader_test.py::TrackPreparationTests::test_does_nothing_if_document_file_available tests/track/loader_test.py::TrackPreparationTests::test_download_document_archive_if_no_file_available tests/track/loader_test.py::TrackPreparationTests::test_download_document_file_if_no_file_available tests/track/loader_test.py::TrackPreparationTests::test_prepare_bundled_document_set_decompresses_compressed_docs tests/track/loader_test.py::TrackPreparationTests::test_prepare_bundled_document_set_does_nothing_if_no_document_files tests/track/loader_test.py::TrackPreparationTests::test_prepare_bundled_document_set_error_compressed_docs_wrong_size tests/track/loader_test.py::TrackPreparationTests::test_prepare_bundled_document_set_if_document_file_available tests/track/loader_test.py::TrackPreparationTests::test_prepare_bundled_document_set_uncompressed_docs_wrong_size tests/track/loader_test.py::TrackPreparationTests::test_raise_download_error_if_no_url_provided_and_file_missing tests/track/loader_test.py::TrackPreparationTests::test_raise_download_error_if_no_url_provided_and_wrong_file_size tests/track/loader_test.py::TrackPreparationTests::test_raise_download_error_if_offline tests/track/loader_test.py::TrackPreparationTests::test_raise_download_error_no_test_mode_file tests/track/loader_test.py::TrackPreparationTests::test_raise_download_error_on_connection_problems tests/track/loader_test.py::TrackPreparationTests::test_raise_error_if_compressed_does_not_contain_expected_document_file tests/track/loader_test.py::TrackPreparationTests::test_raise_error_on_wrong_uncompressed_file_size tests/track/loader_test.py::TemplateRenderTests::test_render_simple_template tests/track/loader_test.py::TemplateRenderTests::test_render_template_with_external_variables tests/track/loader_test.py::TemplateRenderTests::test_render_template_with_globbing tests/track/loader_test.py::TemplateRenderTests::test_render_template_with_variables tests/track/loader_test.py::TrackPostProcessingTests::test_creates_index_auto_management_operations tests/track/loader_test.py::TrackPostProcessingTests::test_post_processes_track_spec tests/track/loader_test.py::TrackPathTests::test_sets_absolute_path tests/track/loader_test.py::TrackFilterTests::test_create_filters_from_empty_included_tasks tests/track/loader_test.py::TrackFilterTests::test_create_filters_from_mixed_included_tasks tests/track/loader_test.py::TrackFilterTests::test_filters_tasks tests/track/loader_test.py::TrackFilterTests::test_rejects_invalid_syntax tests/track/loader_test.py::TrackFilterTests::test_rejects_unknown_filter_type tests/track/loader_test.py::TrackSpecificationReaderTests::test_at_least_one_default_challenge tests/track/loader_test.py::TrackSpecificationReaderTests::test_can_read_track_info tests/track/loader_test.py::TrackSpecificationReaderTests::test_description_is_optional tests/track/loader_test.py::TrackSpecificationReaderTests::test_document_count_mandatory_if_file_present tests/track/loader_test.py::TrackSpecificationReaderTests::test_exactly_one_default_challenge tests/track/loader_test.py::TrackSpecificationReaderTests::test_inline_operations tests/track/loader_test.py::TrackSpecificationReaderTests::test_not_more_than_one_default_challenge_possible tests/track/loader_test.py::TrackSpecificationReaderTests::test_parallel_tasks_with_completed_by_set tests/track/loader_test.py::TrackSpecificationReaderTests::test_parallel_tasks_with_completed_by_set_multiple_tasks_match tests/track/loader_test.py::TrackSpecificationReaderTests::test_parallel_tasks_with_completed_by_set_no_task_matches tests/track/loader_test.py::TrackSpecificationReaderTests::test_parallel_tasks_with_default_clients_does_not_propagate tests/track/loader_test.py::TrackSpecificationReaderTests::test_parallel_tasks_with_default_values tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_challenge_and_challenges_are_defined tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_duplicate_explicit_task_names tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_duplicate_implicit_task_names tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_missing_challenge_or_challenges tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_unique_task_names tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_with_mixed_warmup_iterations_and_measurement tests/track/loader_test.py::TrackSpecificationReaderTests::test_parse_with_mixed_warmup_time_period_and_iterations tests/track/loader_test.py::TrackSpecificationReaderTests::test_selects_sole_challenge_implicitly_as_default tests/track/loader_test.py::TrackSpecificationReaderTests::test_supports_target_interval tests/track/loader_test.py::TrackSpecificationReaderTests::test_supports_target_throughput tests/track/loader_test.py::TrackSpecificationReaderTests::test_types_are_optional_for_user_managed_indices tests/track/loader_test.py::TrackSpecificationReaderTests::test_unique_challenge_names
esrally/config.py esrally/track/loader.py
esrally/config.py esrally/track/loader.py