Repository: green-coding-solutions/green-metrics-tool
Pull request #1088: Add support for entrypoint attribute
Author: login: davidkopp
Created: 2025-03-06T12:44:47Z | Merged: 2025-03-16T08:03:23Z

Closes #1085

Only the short form of the `entrypoint` attribute is implemented (see [Docker Compose specification](https://docs.docker.com/reference/compose-file/services/#entrypoint)).
This is also the case for the `command' attribute, where only the short form is currently implemented.

<!-- greptile_comment -->

## Greptile Summary

Added support for Docker entrypoint configuration in service definitions, enabling overriding of container ENTRYPOINTs to allow execution of runtime commands in test flows.

- Added `entrypoint` validation in `lib/schema_checker.py` as optional string field in service schema
- Modified `runner.py` to handle entrypoint overrides using secure command parsing
- Replaced `clear-entrypoint` flag with standard empty string syntax (`entrypoint: ''`) 
- Updated test scenarios to use `tail -f /dev/null` instead of resource-intensive `yes` command
- Removed custom non-standard attributes to better align with Docker Compose specification



<sub>💡 (5/5) You can turn off certain types of comments like style [here](https://app.greptile.com/review/github)!</sub>

<!-- /greptile_comment -->

Linked issue #1085: Support entrypoint in docker compose
Container images often come with a predefined ENTRYPOINT. The usage of such a container image in a flow within GMT is currently not possible, because in a flow we need to be able to execute a command during runtime of the container and not only at the start of the container.

Therefore, common container images of various API/load testing tools can't be used at the moment because of their ENTRYPOINTs. Examples:
- [JMeter](https://github.com/justb4/docker-jmeter/blob/master/Dockerfile): `ENTRYPOINT ["/entrypoint.sh"]`
- [k6](https://github.com/grafana/k6/blob/master/Dockerfile): `ENTRYPOINT ["k6"]`
- [Artillery](https://github.com/artilleryio/artillery/blob/main/packages/artillery/Dockerfile): `ENTRYPOINT ["/home/node/artillery/bin/run"]`
- [grpcurl](https://github.com/datawire/docker-grpcurl/blob/master/Dockerfile): `ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]`

As far as I know at the moment there is only one solution: Build a custom container image without an entrypoint or an entrypoint that is waiting for commands. I did the latter for JMeter 1,5 years ago here:
https://github.com/t2-project/docker-jmeter/blob/master/entrypoint.sh

A better solution could be the support of the attribute `entrypoint` in the Docker Compose file. By specifying an entrypoint here, the entrypoint in the container image can be overwritten. The attribute is supported by [Docker Compose](https://docs.docker.com/reference/compose-file/services/#entrypoint) and by [docker run](https://docs.docker.com/reference/cli/docker/container/run/#options) (`--entrypoint`).

Is this really a solution or have I overlooked something?
Linked issue comments: user: login: ArneTR
created_at: 2025-03-04T06:47:56Z
body: Jap, a nice feature to have to ease development work a bit.

At the moment GMT can only submit a 'cmd' option, which can override typical CMD in the Dockerfile, but not the ENTRYPOINT, which has precedence.

As you said, atm the way to go is to re-wrap the image and overriden the ENTRYPOINT with an empty string.

+1 on the feature. Will implement it by end of the week. If you are quick feel free to shoot a PR! :)
author_association: MEMBER

Diff:
diff --git a/lib/schema_checker.py b/lib/schema_checker.py
index f699c22e8..46c643496 100644
--- a/lib/schema_checker.py
+++ b/lib/schema_checker.py
@@ -121,6 +121,7 @@ def check_usage_scenario(self, usage_scenario):
                     Optional("setup-commands"): [And(str, Use(self.not_empty))],
                     Optional("volumes"): self.single_or_list(str),
                     Optional("folder-destination"):And(str, Use(self.not_empty)),
+                    Optional("entrypoint"): str,
                     Optional("command"): And(str, Use(self.not_empty)),
                     Optional("log-stdout"): bool,
                     Optional("log-stderr"): bool,
diff --git a/runner.py b/runner.py
index 777d26891..29f77c759 100755
--- a/runner.py
+++ b/runner.py
@@ -961,6 +961,14 @@ def setup_services(self):
                         docker_run_string.append(service['healthcheck']['start_interval'])
 
 
+            if 'entrypoint' in service:
+                if service['entrypoint']:
+                    docker_run_string.append('--entrypoint')
+                    docker_run_string.append(service['entrypoint'])
+                else:
+                    # empty entrypoint -> default entrypoint will be ignored
+                    docker_run_string.append('--entrypoint=')
+
             docker_run_string.append(self.clean_image_name(service['image']))
 
             # Before finally starting the container for the current service, check if the dependent services are ready.
diff --git a/tests/data/entrypoint-application/Dockerfile b/tests/data/entrypoint-application/Dockerfile
new file mode 100644
index 000000000..616808dc5
--- /dev/null
+++ b/tests/data/entrypoint-application/Dockerfile
@@ -0,0 +1,3 @@
+FROM alpine
+RUN apk add stress-ng
+ENTRYPOINT [ "stress-ng", "-c", "1", "-q" ]
diff --git a/tests/data/entrypoint-application/entrypoint-overwrite.sh b/tests/data/entrypoint-application/entrypoint-overwrite.sh
new file mode 100755
index 000000000..0e51af5b1
--- /dev/null
+++ b/tests/data/entrypoint-application/entrypoint-overwrite.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# this file is used in the test case `test_entrypoint_ran_with_file`
+
+tail -f /dev/null
diff --git a/tests/data/usage_scenarios/entrypoint_empty.yml b/tests/data/usage_scenarios/entrypoint_empty.yml
new file mode 100644
index 000000000..17d0d0351
--- /dev/null
+++ b/tests/data/usage_scenarios/entrypoint_empty.yml
@@ -0,0 +1,21 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    entrypoint: '' # ignores entrypoint in container
+    command: tail -f /dev/null
+
+flow:
+  - name: Stress
+    container: test-container
+    commands:
+      - type: console
+        command: stress-ng -c 1 -t 1 -q
+        note: Starting Stress
diff --git a/tests/data/usage_scenarios/entrypoint_script.yml b/tests/data/usage_scenarios/entrypoint_script.yml
new file mode 100644
index 000000000..b251cd20c
--- /dev/null
+++ b/tests/data/usage_scenarios/entrypoint_script.yml
@@ -0,0 +1,22 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    volumes:
+      - ../entrypoint-application/entrypoint-overwrite.sh:/tmp/entrypoint-overwrite.sh
+    entrypoint: /tmp/entrypoint-overwrite.sh # overwrite entrypoint
+
+flow:
+  - name: Stress
+    container: test-container
+    commands:
+      - type: console
+        command: stress-ng -c 1 -t 1 -q
+        note: Starting Stress
diff --git a/tests/data/usage_scenarios/entrypoint_with_command.yml b/tests/data/usage_scenarios/entrypoint_with_command.yml
new file mode 100644
index 000000000..4c639389e
--- /dev/null
+++ b/tests/data/usage_scenarios/entrypoint_with_command.yml
@@ -0,0 +1,21 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    entrypoint: 'tail' # overwrite entrypoint
+    command: '-f /dev/null' # value of command is used as an argument for the entrypoint -> `tail -f /dev/null`
+
+flow:
+  - name: Stress
+    container: test-container
+    commands:
+      - type: console
+        command: stress-ng -c 1 -t 1 -q
+        note: Starting Stress
diff --git a/tests/test_usage_scenario.py b/tests/test_usage_scenario.py
index bbfeb0831..c6211dc5a 100644
--- a/tests/test_usage_scenario.py
+++ b/tests/test_usage_scenario.py
@@ -514,6 +514,62 @@ def test_cmd_ran():
         docker_ps_out = ps.stdout
     assert '1 root      0:00 sh' in docker_ps_out, Tests.assertion_info('1 root      0:00 sh', docker_ps_out)
 
+# entrypoint: [str] (optional)
+#    entrypoint declares the default entrypoint for the service container.
+#    This overrides the ENTRYPOINT instruction from the service's Dockerfile.
+#    If the entrypoint is empty, the ENTRYPOINT instruction is ignored.
+def test_entrypoint_ran_with_script():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_script.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'entrypoint-overwrite.sh' in docker_ps_out, Tests.assertion_info('entrypoint `entrypoint-overwrite.sh` in ps output', docker_ps_out)
+    assert 'tail -f /dev/null' in docker_ps_out, Tests.assertion_info('entrypoint `tail -f /dev/null` in ps output', docker_ps_out)
+
+def test_entrypoint_ran_in_conjunction_with_command():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_with_command.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'tail -f /dev/null' in docker_ps_out, Tests.assertion_info('`tail -f /dev/null` in ps output', docker_ps_out)
+
+def test_entrypoint_empty():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_empty.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    out = io.StringIO()
+    err = io.StringIO()
+    with redirect_stdout(out), redirect_stderr(err):
+        with Tests.RunUntilManager(runner) as context:
+            context.run_until('setup_services')
+            ps = subprocess.run(
+                ['docker', 'exec', 'test-container', 'ps', '-a'],
+                check=True,
+                stderr=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    docker_run_command = re.search(r"docker run with: (.*)", str(out.getvalue())).group(1)
+    assert '--entrypoint= ' in docker_run_command, f"--entrypoint= not found in docker run command: {docker_run_command}"
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been ignored', docker_ps_out)
+    assert 'tail -f /dev/null' in docker_ps_out, Tests.assertion_info('command `tail -f /dev/null` in ps output', docker_ps_out)
+
+
 ### The Tests for the runner options/flags
 ## --uri URI
 #   The URI to get the usage_scenario.yml from. Can be either a local directory starting with


Reviews:
user: login: greptile-apps[bot]
body: <sub>4 file(s) reviewed, 5 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=green-metrics-tool_1088)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T12:56:57Z
user: login: greptile-apps[bot]
body: <sub>10 file(s) reviewed, 7 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=green-metrics-tool_1088)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T11:49:54Z
user: login: greptile-apps[bot]
body: LGTM

<sub>6 file(s) reviewed, no comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=green-metrics-tool_1088)</sub>

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T19:31:53Z

Review comments:
diff_hunk: @@ -0,0 +1,20 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress
+    build:
+      context: ../stress-application
path: tests/data/usage_scenarios/entrypoint_stress.yml
user: login: greptile-apps[bot]
body: style: The relative path '../stress-application' may cause issues if the test is run from different working directories. Consider using an absolute path or a path relative to the repository root.
created_at: 2025-03-06T12:55:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1022,6 +1022,9 @@ def setup_services(self):
                     if health != 'healthy':
                         raise RuntimeError(f"Health check of dependent services of '{service_name}' failed! Container '{dependent_container_name}' is not healthy but '{health}' after waiting for {time_waited} sec! Consider checking your service configuration, the entrypoint of the container or the logs of the container.")
 
+            if 'entrypoint' in service:
+                docker_run_string.extend(shlex.split(service['entrypoint']))
path: runner.py
user: login: greptile-apps[bot]
body: logic: entrypoint should be added before any docker-run-args to match Docker's behavior where entrypoint is processed before additional arguments
created_at: 2025-03-06T12:55:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1022,6 +1022,9 @@ def setup_services(self):
                     if health != 'healthy':
                         raise RuntimeError(f"Health check of dependent services of '{service_name}' failed! Container '{dependent_container_name}' is not healthy but '{health}' after waiting for {time_waited} sec! Consider checking your service configuration, the entrypoint of the container or the logs of the container.")
 
+            if 'entrypoint' in service:
+                docker_run_string.extend(shlex.split(service['entrypoint']))
path: runner.py
user: login: greptile-apps[bot]
body: style: verify that entrypoint is not empty or None before extending docker_run_string

```suggestion
            if 'entrypoint' in service and service['entrypoint']:
                docker_run_string.extend(shlex.split(service['entrypoint']))
```
created_at: 2025-03-06T12:55:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1022,6 +1022,9 @@ def setup_services(self):
                     if health != 'healthy':
                         raise RuntimeError(f"Health check of dependent services of '{service_name}' failed! Container '{dependent_container_name}' is not healthy but '{health}' after waiting for {time_waited} sec! Consider checking your service configuration, the entrypoint of the container or the logs of the container.")
 
+            if 'entrypoint' in service:
+                docker_run_string.extend(shlex.split(service['entrypoint']))
path: runner.py
user: login: greptile-apps[bot]
body: style: check that entrypoint is a string type before calling shlex.split()

```suggestion
            if 'entrypoint' in service and isinstance(service['entrypoint'], str):
                docker_run_string.extend(shlex.split(service['entrypoint']))
```
created_at: 2025-03-06T12:55:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +121,7 @@ def check_usage_scenario(self, usage_scenario):
                     Optional("setup-commands"): [And(str, Use(self.not_empty))],
                     Optional("volumes"): self.single_or_list(str),
                     Optional("folder-destination"):And(str, Use(self.not_empty)),
+                    Optional("entrypoint"): And(str, Use(self.not_empty)),
path: lib/schema_checker.py
user: login: greptile-apps[bot]
body: style: Consider adding validation to ensure entrypoint doesn't contain shell syntax that could be unsafe (similar to contains_no_invalid_chars check)

```suggestion
                    Optional("entrypoint"): And(str, Use(self.not_empty), Use(self.contains_no_invalid_chars)),
```
created_at: 2025-03-06T12:55:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,3 @@
+FROM alpine
+RUN apk add stress-ng
+ENTRYPOINT [ "stress-ng", "-c", "1", "-q" ]
path: tests/data/entrypoint-application/Dockerfile
user: login: greptile-apps[bot]
body: style: Consider making the stress-ng parameters configurable via environment variables to allow more flexible testing scenarios without rebuilding the image
created_at: 2025-03-15T11:48:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
path: tests/data/usage_scenarios/clear_entrypoint.yml
user: login: greptile-apps[bot]
body: style: Description should be more specific about testing entrypoint clearing functionality
created_at: 2025-03-15T11:49:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    entrypoint: 'yes' # overwrite entrypoint; yes prints "y" forever
path: tests/data/usage_scenarios/entrypoint_single_command.yml
user: login: greptile-apps[bot]
body: style: Using 'yes' as an entrypoint could cause issues with memory and CPU usage before the actual stress test begins, potentially affecting measurements. Consider using a more neutral entrypoint like 'sleep infinity'.
created_at: 2025-03-15T11:49:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    entrypoint: 'yes' # overwrite entrypoint
+    command: 'Running forever...' # value of command is used as an argument for the entrypoint -> `yes Running forever...`
path: tests/data/usage_scenarios/entrypoint_with_command.yml
user: login: greptile-apps[bot]
body: style: The 'yes' command with argument will continuously write to stdout. This could potentially fill up logs and cause performance issues if not properly managed.
created_at: 2025-03-15T11:49:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+---
+name: Test Stress
+author: David Kopp
+description: test
+
+services:
+  test-container:
+    type: container
+    image: gcb_stress_entrypoint # image with an endpoint
+    build:
+      context: ../entrypoint-application
+    entrypoint: 'yes' # overwrite entrypoint
+    command: 'Running forever...' # value of command is used as an argument for the entrypoint -> `yes Running forever...`
+
+flow:
+  - name: Stress
+    container: test-container
+    commands:
+      - type: console
+        command: stress-ng -c 1 -t 1 -q
path: tests/data/usage_scenarios/entrypoint_with_command.yml
user: login: greptile-apps[bot]
body: logic: stress-ng command may fail if container is busy with 'yes' output. Consider adding ignore-errors: true or implementing output redirection for the entrypoint
created_at: 2025-03-15T11:49:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -514,6 +514,90 @@ def test_cmd_ran():
         docker_ps_out = ps.stdout
     assert '1 root      0:00 sh' in docker_ps_out, Tests.assertion_info('1 root      0:00 sh', docker_ps_out)
 
+# entrypoint: [str] (optional)
+#    entrypoint declares the default entrypoint for the service container.
+#    This overrides the ENTRYPOINT instruction from the service's Dockerfile.
+def test_entrypoint_ran():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_single_command.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'yes' in docker_ps_out, Tests.assertion_info('entrypoint `yes` in ps output', docker_ps_out)
path: tests/test_usage_scenario.py
user: login: greptile-apps[bot]
body: style: assertion message should be more specific about what was overwritten (the entrypoint) rather than just saying 'it'
created_at: 2025-03-15T11:49:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -514,6 +514,90 @@ def test_cmd_ran():
         docker_ps_out = ps.stdout
     assert '1 root      0:00 sh' in docker_ps_out, Tests.assertion_info('1 root      0:00 sh', docker_ps_out)
 
+# entrypoint: [str] (optional)
+#    entrypoint declares the default entrypoint for the service container.
+#    This overrides the ENTRYPOINT instruction from the service's Dockerfile.
+def test_entrypoint_ran():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_single_command.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'yes' in docker_ps_out, Tests.assertion_info('entrypoint `yes` in ps output', docker_ps_out)
+
+def test_entrypoint_ran_with_script():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_script.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'entrypoint-overwrite.sh' in docker_ps_out, Tests.assertion_info('entrypoint `entrypoint-overwrite.sh` in ps output', docker_ps_out)
+    assert 'tail -f /dev/null' in docker_ps_out, Tests.assertion_info('entrypoint `tail -f /dev/null` in ps output', docker_ps_out)
+
+def test_entrypoint_ran_in_conjunction_with_command():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_with_command.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    with Tests.RunUntilManager(runner) as context:
+        context.run_until('setup_services')
+        ps = subprocess.run(
+            ['docker', 'exec', 'test-container', 'ps', '-a'],
+            check=True,
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    assert 'stress-ng' not in docker_ps_out, Tests.assertion_info('`stress-ng` should not be in ps output, as it should have been overwritten', docker_ps_out)
+    assert 'yes Running forever...' in docker_ps_out, Tests.assertion_info('`yes Running forever...` in ps output', docker_ps_out)
+
+def test_entrypoint_empty():
+    runner = Runner(uri=GMT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/entrypoint_empty.yml', skip_system_checks=True, dev_no_metrics=True, dev_no_phase_stats=True, dev_no_sleeps=True, dev_cache_build=True)
+    out = io.StringIO()
+    err = io.StringIO()
+    with redirect_stdout(out), redirect_stderr(err):
+        with Tests.RunUntilManager(runner) as context:
+            context.run_until('setup_services')
+            ps = subprocess.run(
+                ['docker', 'exec', 'test-container', 'ps', '-a'],
+                check=True,
+                stderr=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                encoding='UTF-8'
+        )
+        docker_ps_out = ps.stdout
+    docker_run_command = re.search(r"docker run with: (.*)", str(out.getvalue())).group(1)
+    assert '--entrypoint= ' in docker_run_command, f"--entrypoint= not found in docker run command: {docker_run_command}"
path: tests/test_usage_scenario.py
user: login: greptile-apps[bot]
body: style: regex pattern could fail if docker command contains multiple --entrypoint flags. Consider using word boundaries \b in pattern
created_at: 2025-03-15T11:49:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: ArneTR
created_at: 2025-03-06T12:53:28Z
body: Uh, this looks juicy, ty!

1. I will let our GreptileAI check it ... so beware for maybe incoming comments.
2. On first view I believe using shlex.split on the entrypoint may introduce an argument injection. The idea is to have whatever is supplied as entrypoint to be supplied as one contigous string.
=> I will give it a more thorough look tomorrow morning

First and foremost: Big thx!
author_association: MEMBER
user: login: ArneTR
created_at: 2025-03-06T12:53:34Z
body: @greptileai
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-06T22:36:01Z
body: My initial implementation was completely wrong. Sorry for that.
Before marking the PR as ready I would like to do some more tests. For example I would like to set the `entrypoint` to `entrypoint.sh` to be able to execute arbitrary commands instead of just one word.
author_association: CONTRIBUTOR
user: login: davidkopp
created_at: 2025-03-08T16:05:56Z
body: I think the PR is now ready for review. I added now 3 unit tests in total to have a good understanding of how the `entrypoint` can be used, also in conjunction with `command`.
Let me know, if 3 tests are too much because of the time the tests are taking.

I didn't do the changes greptile proposed, because then it would not be consistent with the rest of the code.
author_association: CONTRIBUTOR
user: login: ribalba
created_at: 2025-03-10T07:40:06Z
body: Cool idea and great way to KISS it. Something I do see myself wanting to use is a syntax like
```
    entrypoint: tail -f /dev/null
```
so I don't need to create a separate shell file. You did that in [your first version](https://github.com/green-coding-solutions/green-metrics-tool/pull/1088/commits/b309d45b0b507af34f7d8ee087317536922d7960#diff-34cde5e49b54d9b67952c80d3580b6c421ebcf16097996e215aef54e44f78562R1026) I think we should allow scripts and commands in the specification
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-10T09:25:18Z
body: My first version was wrong. I added the entrypoint as CMD by mistake.
If we would allow `entrypoint: tail -f /dev/null` in GMT, it would violate the [Docker Compose specification](https://docs.docker.com/reference/compose-file/services/#entrypoint). 

If we want to be compliant, I think there are only the following 3 ways to have an entrypoint like `tail -f /dev/null`:

- use a script
   ```yaml
   entrypoint: entrypoint.sh # contains the full command: tail -f /dev/null
   ```
- use `entrypoint` in conjunction with `command`:
   ```yaml
   entrypoint: tail
   command: -f /dev/null
   ```
- use long form of entrypoint (not yet implemented):
   ```yaml
   entrypoint:
     - tail
     - -f
     - /dev/null
   ```

Do we want to add support of the long form for `entrypoint` and `command`?
author_association: CONTRIBUTOR
user: login: ribalba
created_at: 2025-03-10T10:07:32Z
body: I was thinking of then changing it to a list internally with the split. But we should keep the standard 

But maybe we should step back a step and think about how this feature would be used. We could still support the std compose entry point for people that want to use that in their compose file. 

But the main usage IMHO will be to keep the docker container running when we are using a container that normally exits after command completion. So maybe we should either:
a) have a std script in the docker container that everyone can use. Something like /tmp/run4ever.sh
b) add a new key to the compose section. Something like `run_container_4_ever: true` which then overwrites the entry point with `['tail', ...`. 
c) Creates a new docker file with the entrypoint changed. Not really in favour of doing this except if we can come up with some other things we might want to add to a new docker container for certain benchmarks or that makes something easier. But I can't think of something currently so I would not implement it like this. 
 
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-10T10:58:19Z
body: I like the idea to provide a simple way to keep the container alive.
Option a and b would be both ok for me. Option b, setting an attribute to true in the service definition, would be the easiest way from a user perspective. The only concern I have with this approach is that the values provided as `entrypoint` and `command` must be ignored.
An alternative name for the attribute could be `keep_alive` or `keep_container_alive`.

So which approach do we want to take?

I came across [this statement](https://stackoverflow.com/a/41655546), that suggests to use `sleep` or a while loop with sleep instead of using `tail` for blocking. `sleep infinity` does not work on all systems (e.g. busybox), so maybe a sleep in a while loop would be the best option to implement it.
author_association: CONTRIBUTOR
user: login: ribalba
created_at: 2025-03-10T11:09:24Z
body: - We could output a warning if we detect that `entrypoint` is set to make it clear to the user. 
- I like "keep_container_alive" far more than my hastily written idea
- I can't remember why I moved to the `tail` syntax at some stage. I used to use sleep but that failed on some systems. But I can't remember what that was. I would propose to go with the `sleep` for now till we encounter a problem as the `tail` solution does add overhead. 

@ArneTR is currently out so let's wait and see what he has to say about this :) I can imagine him having an opinion.
author_association: MEMBER
user: login: ArneTR
created_at: 2025-03-12T13:58:55Z
body: Hey Hey, sorry for the late response but I am sadly sick and thus response times are bad ...

1. Long syntax
This is only possible as a workaround as in the end we pass params to the CLI, which has no native support for it. So we need to manually split it up ... I would prefer not to go that route

2. The idea of having a declarative argument like "keep_container_alive" is nice but overloads a lot of stuff whic h is unclear without readin the docs.
Furthermore I am not 100% sure if we can always detect if an entrypoint is set in the Dockerfile. 

3. My proposal would be instead to introduce the overhead of trying to implement entrypoint we just implement an entrypoint "remover".
All the functionality that entrypoint has can be mimicked with just using cmd.
So I propose introducing *clear_entrypoint* and then a user can craft a usage_scenario with a *cmd* command as wanted.
This saves users from re-building their container images, allows to run different commands as needed, and introduces no escaping need or security guarding in GMT.
Only downside I see so far is that the argument is non declarative and a user needs to go to the docs to understand how to keep a container running. We could however catch this state and output a hint ...?

@davidkopp Since you have the feature currently in use: Would that implementation in GMT work for you?
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-14T23:00:32Z
body: An entrypoint remover would also work for me.

There are now multiple ways implemented in this PR that would work for me:

1. script:
   ```yaml
   entrypoint: entrypoint.sh
   ```

2. single word entrypoint and arguments provided with the command attribute:
   ```yaml
   entrypoint: tail
   command: -f /dev/null
   ``` 

3. empty entrypoint (ignores default entrypoint):
   ```yaml
   entrypoint: ""
   command: tail -f /dev/null
   ``` 

4. explicit entrypoint remover:
   ```yaml
   clear-entrypoint: true
   command: tail -f /dev/null
   ````    

The ways 1-3 are compliant with the Docker Compose specification, 4 not.

I personally like the option 3 the most. What do you think about it? If you like it as well, we would not need the custom attribute `clear-entrypoint` and we could remove it again.
But the explicit `clear-entrypoint` attribute would be also ok for me.

btw: Option 3 and 4 are implemented internally the same way.
author_association: CONTRIBUTOR
user: login: ArneTR
created_at: 2025-03-15T11:47:33Z
body: Oh nice thinking. Yah, option 3 is way nicer and does introduce mental overhead with more attributes.

Happy to merge in variant 3 if you trim the PR down. Can also release it today then.

But first of all: Will run the tests in the current form and call Greptile.
author_association: MEMBER
user: login: ArneTR
created_at: 2025-03-15T11:48:06Z
body: @greptileai
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-15T16:29:57Z
body: I removed the attribute `clear-entrypoint` again and removed one test case, that is not really relevant.

So from my side the PR would be ready to merge 😊

author_association: CONTRIBUTOR
user: login: davidkopp
created_at: 2025-03-15T16:35:22Z
body: Ah, one last question: Should there be a check if the entrypoint consists only of a single word? If there are multiple words it will be a problem later on when the container is created with a incorrect docker run command.
author_association: CONTRIBUTOR
user: login: ArneTR
created_at: 2025-03-15T19:30:42Z
body: @greptileai
author_association: MEMBER
user: login: ArneTR
created_at: 2025-03-16T07:53:44Z
body: Code looks great and I will be merged. 

Here my replies to your comments:
- As you noted starting the container will fail if more than one word is in the entrypoint. This is expected for the CLI calling, but is different from how docker compose behaves. I argue that it is fine for now and I would like to introduce this functionality step by step. The PR is already touching some files.
  - For instance, different to how I understood you earlier, this code behaves totally fine in Docker Compose:
  ```docker
    name: Stress Container One Core 5 Seconds
    services:
      ubuntu:
        image: ubuntu
        entrypoint: 'echo "asd"'
        command: "what"
  ```
  - It will output "asd what". Docker just merges entrypoint and command, even when entrypoint is just a string and has more than one word. It does not read like that from the docs, but it seems the implemented behaviour.
  - Although I find this quirky, this is they way GMT should also behave I believe ... I will create this in a follow up PR

- Secondly: One major problem I see, not limited to this PR though, is that WHEN only a single word is supplied the error is very misleading:
```log
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Error: RuntimeError occured in runner.py

Exception (RuntimeError): PowermetricsProvider returned error message: Metrics provider powermetrics seems to have not produced any measurements. Metrics log file was empty. Either consider having a higher sample rate or turn off provider.
...
``` 

Since multiple exceptions happended in this case Python shows only the last hereby masking what was actually the root cause.

I will add the exception context to make this clearer thus resulting in something like this:

```log
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Error: RuntimeError occured in runner.py

Exception (RuntimeError): PowermetricsProvider returned error message: Metrics provider powermetrics seems to have not produced any measurements. Metrics log file was empty. Either consider having a higher sample rate or turn off provider.
Initial_exceptions (OSError): Docker run failed
Stderr: docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "sleep asd": executable file not found in $PATH: unknown.

```
author_association: MEMBER
user: login: davidkopp
created_at: 2025-03-16T10:13:22Z
body: Thanks for merging!

> Docker just merges entrypoint and command, even when entrypoint is just a string and has more than one word. It does not read like that from the docs, but it seems the implemented behaviour.

You are absolutely right. I missed that point that if there are more than one word in the entrypoint, they will become the arguments, together with cmd.
To verify that I checked the [implementation of the Docker engine](https://github.com/moby/moby/blob/3e1b15dc97630946c87ff7c4c1bf43128d908806/daemon/container.go#L176):

```go
return configEntrypoint[0], append(configEntrypoint[1:], configCmd...)
``` 

Only the first element of the entrypoint configuration list is used as the final entrypoint. The rest are used as arguments, together with the cmd configuration.
author_association: CONTRIBUTOR