Repository: jupyterhub/kubespawner

Issue:
Add configuration for annotations on spawned singleuser pods
The spawner supports [configuring extra labels on spawned pods](https://github.com/jupyterhub/kubespawner/blob/06a2e09bc05e873a3e9e8b29d60beceb193fb2b7/kubespawner/spawner.py#L264-L280).

It should also support configuring extra annotations on spawned pods.

Comments:

Patch:
diff --git a/kubespawner/objects.py b/kubespawner/objects.py
index e2632b7..0f34a49 100644
--- a/kubespawner/objects.py
+++ b/kubespawner/objects.py
@@ -36,6 +36,7 @@ def make_pod(
     volumes=[],
     volume_mounts=[],
     labels={},
+    annotations={},
     cpu_limit=None,
     cpu_guarantee=None,
     mem_limit=None,
@@ -97,6 +98,8 @@ def make_pod(
         String specifying the working directory for the notebook container
       - labels:
         Labels to add to the spawned pod.
+      - annotations:
+        Annotations to add to the spawned pod.
       - cpu_limit:
         Float specifying the max number of CPU cores the user's pod is
         allowed to use.
@@ -131,6 +134,8 @@ def make_pod(
     pod.metadata = V1ObjectMeta()
     pod.metadata.name = name
     pod.metadata.labels = labels.copy()
+    if annotations:
+        pod.metadata.annotations = annotations.copy()
 
     pod.spec = V1PodSpec()
 
diff --git a/kubespawner/spawner.py b/kubespawner/spawner.py
index c2bffd9..4f75887 100644
--- a/kubespawner/spawner.py
+++ b/kubespawner/spawner.py
@@ -279,6 +279,23 @@ class KubeSpawner(Spawner):
         """
     )
 
+    singleuser_extra_annotations = Dict(
+        {},
+        config=True,
+        help="""
+        Extra kubernetes annotations to set on the spawned single-user pods.
+
+        The keys and values specified here are added as annotations on the spawned single-user
+        kubernetes pods. The keys and values must both be strings.
+
+        See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ for more
+        info on what annotations are and why you might want to use them!
+
+        {username} and {userid} are expanded to the escaped, dns-label safe
+        username & integer user id respectively, wherever they are used.
+        """
+    )
+
     singleuser_image_spec = Unicode(
         'jupyterhub/singleuser:latest',
         config=True,
@@ -764,6 +781,7 @@ class KubeSpawner(Spawner):
             real_cmd = None
 
         labels = self._build_pod_labels(self._expand_all(self.singleuser_extra_labels))
+        annotations = self._expand_all(self.singleuser_extra_annotations)
 
         return make_pod(
             name=self.pod_name,
@@ -781,6 +799,7 @@ class KubeSpawner(Spawner):
             volume_mounts=self._expand_all(self.volume_mounts),
             working_dir=self.singleuser_working_dir,
             labels=labels,
+            annotations=annotations,
             cpu_limit=self.cpu_limit,
             cpu_guarantee=self.cpu_guarantee,
             mem_limit=self.mem_limit,


Test patch:
diff --git a/tests/test_objects.py b/tests/test_objects.py
index cc0bde8..c861b7a 100644
--- a/tests/test_objects.py
+++ b/tests/test_objects.py
@@ -90,6 +90,49 @@ def test_make_labeled_pod():
         "apiVersion": "v1"
     }
 
+def test_make_annotated_pod():
+    """
+    Test specification of the simplest possible pod specification with annotations
+    """
+    assert api_client.sanitize_for_serialization(make_pod(
+        name='test',
+        image_spec='jupyter/singleuser:latest',
+        cmd=['jupyterhub-singleuser'],
+        port=8888,
+        image_pull_policy='IfNotPresent',
+        annotations={"test": "true"}
+    )) == {
+        "metadata": {
+            "name": "test",
+            "annotations": {"test": "true"},
+            "labels": {},
+        },
+        "spec": {
+            "securityContext": {},
+            "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_make_pod_with_image_pull_secrets():
     """
     Test specification of the simplest possible pod specification