diff --git a/README.rst b/README.rst index 64c9246..57eb847 100644 --- a/README.rst +++ b/README.rst @@ -121,6 +121,10 @@ The ``[docker:container-name]`` section may contain the following directives: test run until the container reports healthy, and will fail the test run if it never does so (within the parameters specified). +``user`` + The `user`__ Username + or UID to run commands as inside the container. + Command-Line Arguments ---------------------- @@ -182,6 +186,8 @@ Example # testing use cases, as this could persist data between test runs volumes = bind:rw:/my/own/datadir:/var/lib/postgresql/data + # The uid (or username) to run commands as inside the container. + user = 1234 [docker:appserv] # You can use any value that `docker run` would accept as the image diff --git a/tox_docker/config.py b/tox_docker/config.py index 1721a1d..218fc9a 100644 --- a/tox_docker/config.py +++ b/tox_docker/config.py @@ -19,6 +19,9 @@ r"$" ) +# uid or username. valid username regex from useradd(8) +USER = re.compile(r"[a-z_][a-z0-9_-]*[$]?|[0-9]+") + def runas_name(container_name: str, pid: Optional[int] = None) -> str: """ @@ -103,6 +106,19 @@ def __init__(self, config_line: str) -> None: ) +class User: + def __init__(self, config_line: str) -> None: + match = USER.match(config_line) + if not match: + raise ValueError(f"{config_line!r} is not a valid user name") + self.username = config_line + self.uid = None + try: + self.uid = int(self.username) + except ValueError: + pass + + class ContainerConfig: def __init__( self, @@ -118,6 +134,7 @@ def __init__( ports: Optional[Collection[Port]] = None, links: Optional[Collection[Link]] = None, volumes: Optional[Collection[Volume]] = None, + user: Optional[User] = None, ) -> None: self.name = name self.runas_name = runas_name(name) @@ -139,3 +156,4 @@ def __init__( int(healthcheck_start_period) if healthcheck_start_period else None ) self.healthcheck_retries = healthcheck_retries + self.user = user diff --git a/tox_docker/plugin.py b/tox_docker/plugin.py index eec57fd..1b013d6 100644 --- a/tox_docker/plugin.py +++ b/tox_docker/plugin.py @@ -97,6 +97,10 @@ def docker_run( if not os.path.exists(source): raise ValueError(f"Volume source {source!r} does not exist") + user = None + if container_config.user: + user = container_config.user.uid or container_config.user.username + log(f"run {container_config.image!r} (from {container_config.name!r})") container = docker.containers.run( str(container_config.image), @@ -108,6 +112,7 @@ def docker_run( ports=ports, publish_all_ports=len(ports) == 0, mounts=container_config.mounts, + user=user, ) container.reload() # TODO: why do we need this? return container diff --git a/tox_docker/tox3/config.py b/tox_docker/tox3/config.py index eaa4850..64a972b 100644 --- a/tox_docker/tox3/config.py +++ b/tox_docker/tox3/config.py @@ -11,6 +11,7 @@ Link, Port, RunningContainers, + User, Volume, ) @@ -133,6 +134,10 @@ def parse_container_config( if reader.getstring("volumes"): volumes = [Volume(line) for line in reader.getlist("volumes")] + user = None + if reader.getstring("user"): + user = User(reader.getstring("user")) + return ContainerConfig( name=container_name, image=Image(reader.getstring("image")), @@ -146,4 +151,5 @@ def parse_container_config( ports=ports, links=links, volumes=volumes, + user=user, ) diff --git a/tox_docker/tox4/config.py b/tox_docker/tox4/config.py index 8beb390..ba0bf73 100644 --- a/tox_docker/tox4/config.py +++ b/tox_docker/tox4/config.py @@ -2,7 +2,7 @@ from tox.config.sets import ConfigSet -from tox_docker.config import ContainerConfig, Image, Link, Port, Volume +from tox_docker.config import ContainerConfig, Image, Link, Port, User, Volume # nanoseconds in a second; named "SECOND" so that "1.5 * SECOND" makes sense SECOND = 1000000000 @@ -86,6 +86,12 @@ def register_config(self) -> None: default=0, desc="docker healthcheck retry count", ) + self.add_config( + keys=["user"], + of_type=User, + default=None, + desc="Username or UID to run commands as inside the container", + ) def parse_container_config(docker_config: DockerConfigSet) -> ContainerConfig: @@ -102,4 +108,5 @@ def parse_container_config(docker_config: DockerConfigSet) -> ContainerConfig: ports=docker_config["ports"], links=docker_config["links"], volumes=docker_config["volumes"], + user=docker_config["user"], )