Skip to content

Commit a41f9e3

Browse files
linusseelingerjohnbcoughlinxalelaxdionhaefner
authored
feat: pass env variables through tesseract run and tesseract serve (#250)
#### Relevant issue or PR @johnbcoughlin extracted this from #229 . It's needed right away and requires less discussion, so makes sense to merge separately. #### Description of changes We allow passing user-defined env variables to docker (and docker-compose) on `tesseract run` and `tesseract serve`. Example: ``` tesseract run --env=MLFLOW_TRACKING_URI="http://localhost:5001" helloworld apply '...' ``` Immediately relevant use case: Connecting tesseracts to user-hosted MLflow servers. #### Testing done Tested manually. --------- Co-authored-by: Jack Coughlin <jack@johnbcoughlin.com> Co-authored-by: Alessandro Angioi <alessandro.angioi@simulation.science> Co-authored-by: Dion Häfner <dion.haefner@simulation.science>
1 parent cdd451b commit a41f9e3

File tree

8 files changed

+118
-3
lines changed

8 files changed

+118
-3
lines changed

.github/workflows/run_tests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ jobs:
114114
--skip-endtoend \
115115
--cov-report=term-missing:skip-covered \
116116
--cov-report=xml:coverage.xml \
117-
--cov=tesseract_core
117+
--cov=tesseract_core \
118+
--cov-branch
118119
119120
- name: Run test suite (Windows)
120121
if: runner.os == 'Windows'
@@ -125,7 +126,8 @@ jobs:
125126
--skip-endtoend \
126127
--cov-report=term-missing:skip-covered \
127128
--cov-report=xml:coverage.xml \
128-
--cov=tesseract_core
129+
--cov=tesseract_core \
130+
--cov-branch
129131
130132
- name: Upload coverage reports to Codecov
131133
uses: codecov/codecov-action@v5.4.3
@@ -279,6 +281,7 @@ jobs:
279281
if [ "${{ matrix.unit-tesseract }}" == "base" ]; then
280282
uv run --no-sync pytest \
281283
--always-run-endtoend \
284+
--cov-branch \
282285
--cov-report=term-missing:skip-covered \
283286
--cov-report=xml:coverage.xml \
284287
--cov=tesseract_core \
@@ -287,6 +290,7 @@ jobs:
287290
else
288291
uv run --no-sync pytest \
289292
--always-run-endtoend \
293+
--cov-branch \
290294
--cov-report=term-missing:skip-covered \
291295
--cov-report=xml:coverage.xml \
292296
--cov=tesseract_core \

docs/content/using-tesseracts/advanced.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ target path:
1616
$ tesseract run vectoradd apply --output-path /tmp/output @inputs.json
1717
```
1818

19+
## Passing environment variables to Tesseract containers
20+
21+
Through the optional `--env` argument, you can pass environment variables to Tesseracts.
22+
This works both for serving a Tesseract and running a single execution:
23+
24+
```bash
25+
$ tesseract serve --env=MY_ENV_VARIABLE="some value" helloworld
26+
$ tesseract run --env=MY_ENV_VARIABLE="some value" helloworld apply '{"inputs": {"name": "Osborne"}}'
27+
```
28+
1929
## Using GPUs
2030

2131
To leverage GPU support in your Tesseract environment, you can specify which NVIDIA GPU(s) to make available

tesseract_core/sdk/cli.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ def serve(
436436
show_default=False,
437437
),
438438
] = None,
439+
environment: Annotated[
440+
list[str] | None,
441+
typer.Option(
442+
"--env",
443+
"-e",
444+
help="Set environment variables in the Tesseract containers, in Docker format: key=value.",
445+
),
446+
] = None,
439447
port: Annotated[
440448
str | None,
441449
typer.Option(
@@ -542,6 +550,19 @@ def serve(
542550
else:
543551
ports = None
544552

553+
# Parse environment variables from list to dict
554+
if environment is not None:
555+
try:
556+
environment = {
557+
env.split("=", maxsplit=1)[0]: env.split("=", maxsplit=1)[1]
558+
for env in environment
559+
}
560+
except Exception as ex:
561+
raise typer.BadParameter(
562+
"Environment variables must be in the format 'key=value'.",
563+
param_hint="environment",
564+
) from ex
565+
545566
if service_names is not None:
546567
if no_compose:
547568
raise typer.BadParameter(
@@ -559,6 +580,7 @@ def serve(
559580
host_ip,
560581
ports,
561582
volume,
583+
environment,
562584
gpus,
563585
debug,
564586
num_workers,
@@ -840,6 +862,16 @@ def run_container(
840862
),
841863
),
842864
] = None,
865+
environment: Annotated[
866+
list[str] | None,
867+
typer.Option(
868+
"--env",
869+
"-e",
870+
help="Set environment variables in the Tesseract container, in Docker format: key=value.",
871+
metavar="key=value",
872+
show_default=False,
873+
),
874+
] = None,
843875
user: Annotated[
844876
str | None,
845877
typer.Option(
@@ -888,9 +920,26 @@ def run_container(
888920
)
889921
raise typer.BadParameter(error_string, param_hint="cmd")
890922

923+
if environment is not None:
924+
try:
925+
environment = {
926+
item.split("=", maxsplit=1)[0]: item.split("=", maxsplit=1)[1]
927+
for item in environment
928+
}
929+
except Exception as ex:
930+
raise typer.BadParameter(
931+
"Environment variables must be in the format 'key=value'.",
932+
param_hint="env",
933+
) from ex
891934
try:
892935
result_out, result_err = engine.run_tesseract(
893-
tesseract_image, cmd, args, volumes=volume, gpus=gpus, user=user
936+
tesseract_image,
937+
cmd,
938+
args,
939+
volumes=volume,
940+
gpus=gpus,
941+
environment=environment,
942+
user=user,
894943
)
895944

896945
except ImageNotFound as e:

tesseract_core/sdk/docker_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ def run(
507507
command: list_[str],
508508
volumes: dict | None = None,
509509
device_requests: list_[int | str] | None = None,
510+
environment: dict[str, str] | None = None,
510511
detach: bool = False,
511512
remove: bool = False,
512513
ports: dict | None = None,
@@ -565,6 +566,12 @@ def run(
565566
gpus_str = ",".join(device_requests)
566567
optional_args.extend(["--gpus", f'"device={gpus_str}"'])
567568

569+
if environment:
570+
env_args = []
571+
for env_var, value in environment.items():
572+
env_args.extend(["-e", f"{env_var}={value}"])
573+
optional_args.extend(env_args)
574+
568575
# Remove and detached cannot both be set to true
569576
if remove and detach:
570577
raise ValueError(

tesseract_core/sdk/engine.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ def serve(
544544
host_ip: str = "127.0.0.1",
545545
ports: list[str] | None = None,
546546
volumes: list[str] | None = None,
547+
environment: dict[str, str] | None = None,
547548
gpus: list[str] | None = None,
548549
debug: bool = False,
549550
num_workers: int = 1,
@@ -560,6 +561,7 @@ def serve(
560561
host_ip: IP address to bind the Tesseracts to.
561562
ports: port or port range to serve each Tesseract on.
562563
volumes: list of paths to mount in the Tesseract container.
564+
environment: dictionary of environment variables to pass to the Tesseract.
563565
gpus: IDs of host Nvidia GPUs to make available to the Tesseracts.
564566
debug: Enable debug mode. This will propagate full tracebacks to the client
565567
and start a debugpy server in the Tesseract.
@@ -671,6 +673,7 @@ def serve(
671673
service_names,
672674
ports,
673675
volumes,
676+
environment,
674677
gpus,
675678
num_workers,
676679
debug=debug,
@@ -697,6 +700,7 @@ def _create_docker_compose_template(
697700
service_names: list[str] | None = None,
698701
ports: list[str] | None = None,
699702
volumes: list[str] | None = None,
703+
environment: dict[str, str] | None = None,
700704
gpus: list[str] | None = None,
701705
num_workers: int = 1,
702706
debug: bool = False,
@@ -756,6 +760,7 @@ def _create_docker_compose_template(
756760
"gpus": gpu_settings,
757761
"environment": {
758762
"TESSERACT_DEBUG": "1" if debug else "0",
763+
**(environment or {}),
759764
},
760765
"num_workers": num_workers,
761766
"debugpy_port": debugpy_ports[i] if debug else None,
@@ -843,6 +848,7 @@ def run_tesseract(
843848
volumes: list[str] | None = None,
844849
gpus: list[int | str] | None = None,
845850
ports: dict[str, str] | None = None,
851+
environment: dict[str, str] | None = None,
846852
user: str | None = None,
847853
) -> tuple[str, str]:
848854
"""Start a Tesseract and execute a given command.
@@ -855,6 +861,8 @@ def run_tesseract(
855861
gpus: list of GPUs, as indices or names, to passthrough the container.
856862
ports: dictionary of ports to bind to the host. Key is the host port,
857863
value is the container port.
864+
environment: list of environment variables to set in the container,
865+
in Docker format: key=value.
858866
user: user to run the Tesseract as, e.g. '1000' or '1000:1000' (uid:gid).
859867
860868
Returns:
@@ -920,6 +928,7 @@ def run_tesseract(
920928
command=cmd,
921929
volumes=parsed_volumes,
922930
device_requests=gpus,
931+
environment=environment,
923932
ports=ports,
924933
detach=False,
925934
remove=True,

tesseract_core/sdk/tesseract.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class SpawnConfig:
2626

2727
image: str
2828
volumes: list[str] | None
29+
environment: dict[str, str] | None
2930
gpus: list[str] | None
3031
num_workers: int
3132
debug: bool
@@ -84,6 +85,7 @@ def from_image(
8485
image: str,
8586
*,
8687
volumes: list[str] | None = None,
88+
environment: dict[str, str] | None = None,
8789
gpus: list[str] | None = None,
8890
num_workers: int = 1,
8991
no_compose: bool = False,
@@ -104,6 +106,7 @@ def from_image(
104106
Args:
105107
image: The Docker image to use.
106108
volumes: List of volumes to mount, e.g. ["/path/on/host:/path/in/container"].
109+
environment: dictionary of environment variables to pass to the Tesseract.
107110
gpus: List of GPUs to use, e.g. ["0", "1"]. (default: no GPUs)
108111
num_workers: Number of worker processes to use. This determines how
109112
many requests can be handled in parallel. Higher values
@@ -117,6 +120,7 @@ def from_image(
117120
obj._spawn_config = SpawnConfig(
118121
image=image,
119122
volumes=volumes,
123+
environment=environment,
120124
gpus=gpus,
121125
num_workers=num_workers,
122126
debug=True,
@@ -223,6 +227,7 @@ def serve(self, port: str | None = None, host_ip: str = "127.0.0.1") -> None:
223227
self._spawn_config.image,
224228
port=port,
225229
volumes=self._spawn_config.volumes,
230+
environment=self._spawn_config.environment,
226231
gpus=self._spawn_config.gpus,
227232
num_workers=self._spawn_config.num_workers,
228233
debug=self._spawn_config.debug,
@@ -265,6 +270,7 @@ def _serve(
265270
port: str | None = None,
266271
host_ip: str = "127.0.0.1",
267272
volumes: list[str] | None = None,
273+
environment: dict[str, str] | None = None,
268274
gpus: list[str] | None = None,
269275
debug: bool = False,
270276
num_workers: int = 1,
@@ -279,6 +285,7 @@ def _serve(
279285
[image],
280286
ports=ports,
281287
volumes=volumes,
288+
environment=environment,
282289
gpus=gpus,
283290
debug=debug,
284291
num_workers=num_workers,

tests/endtoend_tests/test_endtoend.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import json
77
import os
8+
import subprocess
89
from pathlib import Path
910

1011
import pytest
@@ -128,6 +129,33 @@ def test_build_generate_only(dummy_tesseract_location, skip_checks):
128129
assert 'RUN ["tesseract-runtime", "check"]' in docker_file_contents
129130

130131

132+
def test_env_passthrough_serve(docker_cleanup, docker_client, built_image_name):
133+
"""Ensure we can pass environment variables to tesseracts when serving."""
134+
run_res = subprocess.run(
135+
[
136+
"tesseract",
137+
"serve",
138+
built_image_name,
139+
"--env=TEST_ENV_VAR=foo",
140+
],
141+
capture_output=True,
142+
text=True,
143+
)
144+
assert run_res.returncode == 0, run_res.stderr
145+
assert run_res.stdout
146+
147+
project_meta = json.loads(run_res.stdout)
148+
project_id = project_meta["project_id"]
149+
tesseract_id = project_meta["containers"][0]["name"]
150+
151+
docker_cleanup["project_ids"].append(project_id)
152+
153+
container = docker_client.containers.get(tesseract_id)
154+
exit_code, output = container.exec_run(["sh", "-c", "echo $TEST_ENV_VAR"])
155+
assert exit_code == 0, f"Command failed with exit code {exit_code}"
156+
assert "foo" in output.decode("utf-8"), f"Output was: {output.decode('utf-8')}"
157+
158+
131159
def test_tesseract_list(built_image_name):
132160
# Test List Command
133161
cli_runner = CliRunner(mix_stderr=False)

tests/sdk_tests/test_tesseract.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def test_serve_lifecycle(mock_serving, mock_clients):
116116
["sometesseract:0.2.3"],
117117
ports=None,
118118
volumes=None,
119+
environment=None,
119120
gpus=None,
120121
debug=True,
121122
num_workers=1,

0 commit comments

Comments
 (0)