Repository: SeldonIO/MLServer

Issue:
Install requirements system-wide when building custom images
Currently, custom dependencies coming from a `requirements.txt` get installed on the base environment for user `1000` (i.e. the default non-`root` user in the image). Therefore, if the image is loaded with a different user ID, these custom dependencies are not available.

To work around this, we should install these custom deps under `root`, so that they are installed system-wide and accessible by any user.


Discussion:

Resolving patch:
diff --git a/mlserver/cli/constants.py b/mlserver/cli/constants.py
--- a/mlserver/cli/constants.py
+++ b/mlserver/cli/constants.py
@@ -46,7 +46,13 @@
     ./requirements.tx[t] \\
     .
 
-RUN ./hack/build-env.sh . ./envs/base
+USER root
+# Install dependencies system-wide, to ensure that they are available for every
+# user
+RUN ./hack/build-env.sh . ./envs/base && \
+    chown -R 1000:0 ./envs/base && \\
+    chmod -R 776 ./envs/base
+USER 1000
 
 # Copy everything else
 COPY . .
diff --git a/runtimes/sklearn/mlserver_sklearn/encoding.py b/runtimes/sklearn/mlserver_sklearn/encoding.py
--- a/runtimes/sklearn/mlserver_sklearn/encoding.py
+++ b/runtimes/sklearn/mlserver_sklearn/encoding.py
@@ -6,7 +6,7 @@
 
 from mlserver.codecs import NumpyCodec, PandasCodec
 from mlserver.errors import InferenceError
-from mlserver.types import ResponseOutput
+from mlserver.types import RequestOutput, ResponseOutput
 
 # TODO: more support!
 SKLearnResponse = Union[np.ndarray, pd.DataFrame]
@@ -17,7 +17,7 @@ class SKLearnPayload:
     """Class for keeping track of requested outputs
     and corresponding model responses."""
 
-    requested_output: ResponseOutput
+    requested_output: RequestOutput
     model_output: SKLearnResponse
 
 
diff --git a/runtimes/sklearn/mlserver_sklearn/sklearn.py b/runtimes/sklearn/mlserver_sklearn/sklearn.py
--- a/runtimes/sklearn/mlserver_sklearn/sklearn.py
+++ b/runtimes/sklearn/mlserver_sklearn/sklearn.py
@@ -83,7 +83,7 @@ def _get_model_outputs(
         for request_output in payload.outputs:  # type: ignore
             predict_fn = getattr(self._model, request_output.name)
             y = predict_fn(decoded_request)
-            payload = SKLearnPayload(requested_output=request_output, model_output=y)
-            outputs.append(payload)
+            output = SKLearnPayload(requested_output=request_output, model_output=y)
+            outputs.append(output)
 
         return outputs


Test patch:
diff --git a/tests/cli/test_build.py b/tests/cli/test_build.py
--- a/tests/cli/test_build.py
+++ b/tests/cli/test_build.py
@@ -1,4 +1,5 @@
 import pytest
+import random
 
 from docker.client import DockerClient
 from pytest_cases import fixture, parametrize_with_cases
@@ -28,12 +29,18 @@ def custom_image(
     docker_client.images.remove(image=image_name, force=True)
 
 
+@pytest.fixture
+def random_user_id() -> int:
+    return random.randint(1000, 65536)
+
+
 @pytest.fixture
 def custom_runtime_server(
     docker_client: DockerClient,
     custom_image: str,
     settings: Settings,
     free_ports: Tuple[int, int],
+    random_user_id: int,
 ) -> str:
     host_http_port, host_grpc_port = free_ports
 
@@ -44,6 +51,7 @@ def custom_runtime_server(
             f"{settings.grpc_port}/tcp": str(host_grpc_port),
         },
         detach=True,
+        user=random_user_id,
     )
 
     yield f"127.0.0.1:{host_http_port}", f"127.0.0.1:{host_grpc_port}"