diff --git a/kubespawner/objects.py b/kubespawner/objects.py index 9c5a59de..76ba5648 100644 --- a/kubespawner/objects.py +++ b/kubespawner/objects.py @@ -30,6 +30,7 @@ def make_pod( node_selector=None, run_as_uid=None, fs_gid=None, + supplemental_gids=None, run_privileged=False, env={}, working_dir=None, @@ -83,6 +84,15 @@ def make_pod( volume types that support this (such as GCE). This should be a group that the uid the process is running as should be a member of, so that it can read / write to the volumes mounted. + supplemental_gids: + A list of GIDs that should be set as additional supplemental groups to + the user that the container runs as. You may have to set this if you are + deploying to an environment with RBAC/SCC enforced and pods run with a + 'restricted' SCC which results in the image being run as an assigned + user ID. The supplemental group IDs would need to include the + corresponding group ID of the user ID the image normally would run as. + The image must setup all directories/files any application needs access + to, as group writable. run_privileged: Whether the container should be run in privileged mode. env: @@ -143,6 +153,8 @@ def make_pod( security_context = V1PodSecurityContext() if fs_gid is not None: security_context.fs_group = int(fs_gid) + if supplemental_gids is not None and supplemental_gids: + security_context.supplemental_groups = [int(gid) for gid in supplemental_gids] if run_as_uid is not None: security_context.run_as_user = int(run_as_uid) pod.spec.security_context = security_context diff --git a/kubespawner/spawner.py b/kubespawner/spawner.py index dc9fd297..56985bc6 100644 --- a/kubespawner/spawner.py +++ b/kubespawner/spawner.py @@ -449,6 +449,32 @@ def _hub_connect_port_default(self): """ ) + singleuser_supplemental_gids = Union([ + List(), + Callable() + ], + allow_none=True, + config=True, + help=""" + A list of GIDs that should be set as additional supplemental groups to the + user that the container runs as. + + Instead of a list of integers, this could also be a callable that takes as one + parameter the current spawner instance and returns a list of integers. The + callable will be called asynchronously if it returns a future, rather than + a list. Note that the interface of the spawner class is not deemed stable + across versions, so using this functionality might cause your JupyterHub + or kubespawner upgrades to break. + + You may have to set this if you are deploying to an environment with RBAC/SCC + enforced and pods run with a 'restricted' SCC which results in the image being + run as an assigned user ID. The supplemental group IDs would need to include + the corresponding group ID of the user ID the image normally would run as. The + image must setup all directories/files any application needs access to, as group + writable. + """ + ) + singleuser_privileged = Bool( False, config=True, @@ -833,6 +859,11 @@ def get_pod_manifest(self): else: singleuser_fs_gid = self.singleuser_fs_gid + if callable(self.singleuser_supplemental_gids): + singleuser_supplemental_gids = yield gen.maybe_future(self.singleuser_supplemental_gids(self)) + else: + singleuser_supplemental_gids = self.singleuser_supplemental_gids + if self.cmd: real_cmd = self.cmd + self.get_args() else: @@ -851,6 +882,7 @@ def get_pod_manifest(self): node_selector=self.singleuser_node_selector, run_as_uid=singleuser_uid, fs_gid=singleuser_fs_gid, + supplemental_gids=singleuser_supplemental_gids, run_privileged=self.singleuser_privileged, env=self.get_env(), volumes=self._expand_all(self.volumes), diff --git a/tests/test_objects.py b/tests/test_objects.py index fff513fa..39515264 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -234,6 +234,54 @@ def test_set_pod_uid_fs_gid(): "apiVersion": "v1" } +def test_set_pod_supplemental_gids(): + """ + Test specification of the simplest possible pod specification + """ + assert api_client.sanitize_for_serialization(make_pod( + name='test', + image_spec='jupyter/singleuser:latest', + cmd=['jupyterhub-singleuser'], + port=8888, + run_as_uid=1000, + supplemental_gids=[100], + image_pull_policy='IfNotPresent' + )) == { + "metadata": { + "name": "test", + "annotations": {}, + "labels": {}, + }, + "spec": { + "securityContext": { + "runAsUser": 1000, + "supplementalGroups": [100] + }, + 'automountServiceAccountToken': False, + "containers": [ + { + "env": [], + "name": "notebook", + "image": "jupyter/singleuser:latest", + "imagePullPolicy": "IfNotPresent", + "args": ["jupyterhub-singleuser"], + "ports": [{ + "name": "notebook-port", + "containerPort": 8888 + }], + 'volumeMounts': [{'name': 'no-api-access-please', 'mountPath': '/var/run/secrets/kubernetes.io/serviceaccount', 'readOnly': True}], + "resources": { + "limits": {}, + "requests": {} + } + } + ], + 'volumes': [{'name': 'no-api-access-please', 'emptyDir': {}}], + }, + "kind": "Pod", + "apiVersion": "v1" + } + def test_run_privileged_container(): """ Test specification of the container to run as privileged