Repository: deepset-ai/haystack

Issue:
KeyError thrown with Docker Compose if no params for DocumentStore is defined
**Describe the bug**
I use `pipelines.haystack-pipeline.yml` file and define an indexing pipeline there. I don't need any `params` for my DocumentStore. However, when I run the `docker-compose.yml` file, `DOCUMENTSTORE_PARAMS_HOST` tries to override the host param of my DocumentStore and throws a KeyError.

**Error message**
```
   | Traceback (most recent call last):
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/arbiter.py", line 589, in spawn_worker
haystack-haystack-api-1   |     worker.init_process()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/uvicorn/workers.py", line 66, in init_process
haystack-haystack-api-1   |     super(UvicornWorker, self).init_process()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/workers/base.py", line 134, in init_process
haystack-haystack-api-1   |     self.load_wsgi()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/workers/base.py", line 146, in load_wsgi
haystack-haystack-api-1   |     self.wsgi = self.app.wsgi()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/app/base.py", line 67, in wsgi
haystack-haystack-api-1   |     self.callable = self.load()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 58, in load
haystack-haystack-api-1   |     return self.load_wsgiapp()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/app/wsgiapp.py", line 48, in load_wsgiapp
haystack-haystack-api-1   |     return util.import_app(self.app_uri)
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/gunicorn/util.py", line 359, in import_app
haystack-haystack-api-1   |     mod = importlib.import_module(module)
haystack-haystack-api-1   |   File "/usr/local/lib/python3.10/importlib/__init__.py", line 126, in import_module
haystack-haystack-api-1   |     return _bootstrap._gcd_import(name[level:], package, level)
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap_external>", line 883, in exec_module
haystack-haystack-api-1   |   File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/rest_api/application.py", line 13, in <module>
haystack-haystack-api-1   |     app = get_app()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/rest_api/utils.py", line 28, in get_app
haystack-haystack-api-1   |     from rest_api.controller import file_upload, search, feedback, document, health
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/rest_api/controller/file_upload.py", line 20, in <module>
haystack-haystack-api-1   |     indexing_pipeline: Pipeline = get_pipelines().get("indexing_pipeline", None)
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/rest_api/utils.py", line 59, in get_pipelines
haystack-haystack-api-1   |     pipelines = setup_pipelines()
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/rest_api/pipeline/__init__.py", line 28, in setup_pipelines
haystack-haystack-api-1   |     query_pipeline = Pipeline.load_from_yaml(Path(config.PIPELINE_YAML_PATH), pipeline_name=config.QUERY_PIPELINE_NAME)
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/haystack/pipelines/base.py", line 1908, in load_from_yaml
haystack-haystack-api-1   |     return cls.load_from_config(
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/haystack/pipelines/base.py", line 1969, in load_from_config
haystack-haystack-api-1   |     validate_config(pipeline_config, strict_version_check=strict_version_check)
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/haystack/pipelines/config.py", line 245, in validate_config
haystack-haystack-api-1   |     component_definitions = get_component_definitions(
haystack-haystack-api-1   |   File "/opt/venv/lib/python3.10/site-packages/haystack/pipelines/config.py", line 83, in get_component_definitions
haystack-haystack-api-1   |     component_definition["params"][param_name] = value
haystack-haystack-api-1   | KeyError: 'params'
```
This error stems from `/haystack/pipelines/config.py` line where `component_definition["params"][param_name] = value`. The presence of `component_definition["params"]` should be checked first.

**Expected behavior**
No error should be thrown and the host param of DocumentStore should be set without any problem

**To Reproduce**
1.  Define DocumentStore without any params in`pipelines.haystack-pipeline.yml` file as follows:
```yml
  - name: DocumentStore
    type: ElasticsearchDocumentStore
  - name: ...
```
2. Define a `DOCUMENTSTORE_PARAMS_HOST`value in `docker-compose.yml` file under haystack-api environment
3. Define volume and `PIPELINE_YAML_PATH` if necessary. See #3544 for details. 
4. Run `docker-compose up`

**FAQ Check**
- [x] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS: macOS
 - GPU/CPU: CPU
 - Haystack version (commit or version number): 1.11



Discussion:

Resolving patch:
diff --git a/haystack/pipelines/config.py b/haystack/pipelines/config.py
--- a/haystack/pipelines/config.py
+++ b/haystack/pipelines/config.py
@@ -79,6 +79,8 @@ def get_component_definitions(
                 env_prefix = f"{name}_params_".upper()
                 if key.startswith(env_prefix):
                     param_name = key.replace(env_prefix, "").lower()
+                    if "params" not in component_definition:
+                        component_definition["params"] = {}
                     component_definition["params"][param_name] = value
                     logger.info(
                         "Param '%s' of component '%s' overwritten with environment variable '%s' value '%s'.",


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -89,7 +89,7 @@ jobs:
       uses: tj-actions/changed-files@v34
       with:
         files: |
-          *.py
+          **/*.py
         files_ignore: |
           test/**
           rest_api/test/**
@@ -100,10 +100,10 @@ jobs:
         pythonVersion: 3.8   # Literals and mypy just don't work well together.
 
     - name: Mypy
-      if: steps.python-files.outputs.changed_files
+      if: steps.files.outputs.any_changed == 'true'
       run: |
         mkdir .mypy_cache/
-        mypy --install-types --non-interactive ${{ steps.python-files.outputs.changed_files }} --exclude=rest_api/build/ --exclude=rest_api/test/
+        mypy --install-types --non-interactive ${{ steps.files.outputs.all_changed_files }} --exclude=rest_api/build/ --exclude=rest_api/test/
 
     - uses: act10ns/slack@v1
       with:
diff --git a/rest_api/test/samples/test.docstore-no-params-pipeline.yml b/rest_api/test/samples/test.docstore-no-params-pipeline.yml
new file mode 100644
--- /dev/null
+++ b/rest_api/test/samples/test.docstore-no-params-pipeline.yml
@@ -0,0 +1,15 @@
+version: 'ignore'
+
+components:
+  - name: InMemoryDocumentStore
+    type: InMemoryDocumentStore
+  - name: tfidf_ret
+    params:
+      document_store: InMemoryDocumentStore
+    type: TfidfRetriever
+pipelines:
+  - name: query
+    nodes:
+      - inputs:
+          - Query
+        name: tfidf_ret
diff --git a/rest_api/test/test_rest_api.py b/rest_api/test/test_rest_api.py
--- a/rest_api/test/test_rest_api.py
+++ b/rest_api/test/test_rest_api.py
@@ -40,6 +40,13 @@ def test_check_error_for_pipeline_not_found():
     assert p is None
 
 
+def test_overwrite_params_with_env_variables_when_no_params_in_pipeline_yaml(monkeypatch):
+    yaml_pipeline_path = Path(__file__).parent.resolve() / "samples" / "test.docstore-no-params-pipeline.yml"
+    monkeypatch.setenv("INMEMORYDOCUMENTSTORE_PARAMS_INDEX", "custom_index")
+    _, document_store = _load_pipeline(yaml_pipeline_path, None)
+    assert document_store.index == "custom_index"
+
+
 def test_bad_yaml_pipeline_configuration_error():
 
     yaml_pipeline_path = Path(__file__).parent.resolve() / "samples" / "test.bogus_pipeline.yml"