Repository: gabrieldemarmiesse/python-on-whales (language: python)

Issue:
Docker container exec --interactive
Hello again,
I was trying to implement a wrapper of docker exec, but I found that some options are missing in the interface currently implemented:

```
docker.container.execute(container, command, detach=False)
```


The target command that I would implement in python is:
```
docker exec --interactive --tty --user myuser mycontainer mycommand
```

In particular options --interactive --tty  and --user are missing and I would like to ask you if there are any plan for the integration

One of the use cases would be the execution of bash / ash to enter the container


Thank you again for your work



Discussion / hints:
Yes, I'm of course totally open to have those options. When I wrote this package I forgot some options here and there. I'll have some time at the end of next week to  implement it, otherwise, as always, I'm open to review pull requests.

If you decide to implement it, don't forget to add the tests in the `tests/` directory. It's straigtforward and there are many examples already :) 
As exercise I managed to add the user option (see the commit above, if you want I can do a PR of this little addition... it's just few lines of code) 

But I don't know how to add the --interactive flag. I tried to add it but once enabled the command hangs and input and output do not work, probably some piping is required somewhere. I will wait for you :)

Fix patch:
diff --git a/python_on_whales/components/container/cli_wrapper.py b/python_on_whales/components/container/cli_wrapper.py
index d62a187a..21547103 100644
--- a/python_on_whales/components/container/cli_wrapper.py
+++ b/python_on_whales/components/container/cli_wrapper.py
@@ -202,13 +202,35 @@ def diff(self) -> Dict[str, str]:
         """
         return ContainerCLI(self.client_config).diff(self)
 
-    def execute(self, command: Union[str, List[str]], detach: bool = False):
+    def execute(
+        self,
+        command: Union[str, List[str]],
+        detach: bool = False,
+        envs: Dict[str, str] = {},
+        env_files: Union[ValidPath, List[ValidPath]] = [],
+        # interactive: bool = False,
+        privileged: bool = False,
+        # tty: bool = False,
+        user: Optional[str] = None,
+        workdir: Optional[ValidPath] = None,
+    ):
         """Execute a command in this container
 
         See the [`docker.container.execute`](../sub-commands/container.md#execute)
         command for information about the arguments.
         """
-        return ContainerCLI(self.client_config).execute(self, command, detach)
+        return ContainerCLI(self.client_config).execute(
+            self,
+            command,
+            detach,
+            envs,
+            env_files,
+            # interactive,
+            privileged,
+            # tty,
+            user,
+            workdir,
+        )
 
     def export(self, output: ValidPath) -> None:
         """Export this container filesystem.
@@ -693,6 +715,13 @@ def execute(
         container: ValidContainer,
         command: Union[str, List[str]],
         detach: bool = False,
+        envs: Dict[str, str] = {},
+        env_files: Union[ValidPath, List[ValidPath]] = [],
+        # interactive: bool = False,
+        privileged: bool = False,
+        # tty: bool = False,
+        user: Optional[str] = None,
+        workdir: Optional[ValidPath] = None,
     ) -> Optional[str]:
         """Execute a command inside a container
 
@@ -703,6 +732,11 @@ def execute(
             command: The command to execute.
             detach: if `True`, returns immediately with `None`. If `False`,
                 returns the command stdout as string.
+            envs: Set environment variables
+            env_files: Read one or more files of environment variables
+            privileged: Give extended privileges to the container.
+            user: Username or UID, format: `"<name|uid>[:<group|gid>]"`
+            workdir: Working directory inside the container
 
         # Returns:
             Optional[str]
@@ -711,6 +745,17 @@ def execute(
 
         full_cmd.add_flag("--detach", detach)
 
+        full_cmd.add_args_list("--env", format_dict_for_cli(envs))
+        full_cmd.add_args_list("--env-file", env_files)
+
+        # TODO: activate interactive and tty
+        # full_cmd.add_flag("--interactive", interactive)
+        full_cmd.add_flag("--privileged", privileged)
+        # full_cmd.add_flag("--tty", tty)
+
+        full_cmd.add_simple_arg("--user", user)
+        full_cmd.add_simple_arg("--workdir", workdir)
+
         full_cmd.append(container)
         for arg in to_list(command):
             full_cmd.append(arg)


Test patch:
diff --git a/tests/python_on_whales/components/test_container.py b/tests/python_on_whales/components/test_container.py
index cb62b87a..36ef2343 100644
--- a/tests/python_on_whales/components/test_container.py
+++ b/tests/python_on_whales/components/test_container.py
@@ -400,3 +400,45 @@ def test_stop_nothing():
 
 def test_kill_nothing():
     docker.container.stop([])
+
+
+def test_exec_env():
+    with docker.run("ubuntu", ["sleep", "infinity"], detach=True, remove=True) as c:
+        result = c.execute(["bash", "-c", "echo $DODO"], envs={"DODO": "dada"})
+
+    assert result == "dada"
+
+
+def test_exec_env_file(tmp_path):
+
+    env_file = tmp_path / "variables.env"
+    env_file.write_text("DODO=dada\n")
+
+    with docker.run("ubuntu", ["sleep", "infinity"], detach=True, remove=True) as c:
+        result = c.execute(["bash", "-c", "echo $DODO"], env_files=[env_file])
+    assert result == "dada"
+
+
+# TODO: fixme
+@pytest.mark.xfail
+def test_exec_privilged_flag():
+    with docker.run(
+        "ubuntu:18.04", ["sleep", "infinity"], detach=True, remove=True
+    ) as c:
+        c.execute(["apt-get", "update"])
+        c.execute(["apt-get", "install", "-y", "iproute2"])
+        c.execute(["ip", "link", "add", "dummy0", "type", "dummy"], privileged=True)
+        c.execute(["ip", "link", "delete", "dummy0"], privileged=True)
+
+
+def test_exec_change_user():
+    with docker.run("ubuntu", ["sleep", "infinity"], detach=True, remove=True) as c:
+        c.execute(["useradd", "-ms", "/bin/bash", "newuser"])
+        assert c.execute(["whoami"], user="newuser") == "newuser"
+
+
+def test_exec_change_directory():
+    with docker.run("ubuntu", ["sleep", "infinity"], detach=True, remove=True) as c:
+        assert c.execute(["pwd"], workdir="/tmp") == "/tmp"
+        assert c.execute(["pwd"], workdir="/etc") == "/etc"
+        assert c.execute(["pwd"], workdir="/usr/lib") == "/usr/lib"