We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
nodeSelector support
I'd like to use node pools on GKE to have a cheaper nodes like `n1-standard-1` host hub related pods and a more expensive nodes host user pods. Is there support for `nodeSelect` pod configuration for the hub and singleuser pods?

https://kubernetes.io/docs/concepts/configuration/assign-pod-node/

My use case is a cluster for long term student projects over the course of the semester, and I'd like to use autoscaler on the expensive node pool to go make it go to zero noes when no students are using the cluster, while leaving the hub up on something cheaper while waiting for a user to log in.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `images/hub/jupyterhub_config.py`
Content:
```
1 import os
2 import sys
3 import yaml
4 from tornado.httpclient import AsyncHTTPClient
5 
6 def get_config(key, default=None):
7     """
8     Find a config item of a given name & return it
9 
10     Parses everything as YAML, so lists and dicts are available too
11     """
12     path = os.path.join('/etc/jupyterhub/config', key)
13     try:
14         with open(path) as f:
15             data = yaml.safe_load(f)
16             print(key, data)
17             return data
18     except FileNotFoundError:
19         return default
20 
21 def get_secret(key, default=None):
22     """Get a secret from /etc/jupyterhub/secret"""
23     path = os.path.join('/etc/jupyterhub/secret', key)
24     try:
25         with open(path) as f:
26             return f.read().strip()
27     except FileNotFoundError:
28         return default
29 
30 
31 # Configure JupyterHub to use the curl backend for making HTTP requests,
32 # rather than the pure-python implementations. The default one starts
33 # being too slow to make a large number of requests to the proxy API
34 # at the rate required.
35 AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
36 
37 c.JupyterHub.spawner_class = 'kubespawner.KubeSpawner'
38 
39 # Connect to a proxy running in a different pod
40 c.ConfigurableHTTPProxy.api_url = 'http://{}:{}'.format(os.environ['PROXY_API_SERVICE_HOST'], int(os.environ['PROXY_API_SERVICE_PORT']))
41 c.ConfigurableHTTPProxy.should_start = False
42 
43 # Check that the proxy has routes appropriately setup
44 # This isn't the best named setting :D
45 c.JupyterHub.last_activity_interval = 60
46 
47 # Max number of servers that can be spawning at any one time
48 c.JupyterHub.concurrent_spawn_limit = get_config('hub.concurrent-spawn-limit')
49 
50 active_server_limit = get_config('hub.active-server-limit', None)
51 
52 if active_server_limit is not None:
53     c.JupyterHub.active_server_limit = int(active_server_limit)
54 
55 c.JupyterHub.ip = os.environ['PROXY_PUBLIC_SERVICE_HOST']
56 c.JupyterHub.port = int(os.environ['PROXY_PUBLIC_SERVICE_PORT'])
57 
58 # the hub should listen on all interfaces, so the proxy can access it
59 c.JupyterHub.hub_ip = '0.0.0.0'
60 
61 c.KubeSpawner.namespace = os.environ.get('POD_NAMESPACE', 'default')
62 
63 # Sometimes disks take a while to attach, so let's keep a not-too-short timeout
64 c.KubeSpawner.start_timeout = 5 * 60
65 
66 # Use env var for this, since we want hub to restart when this changes
67 c.KubeSpawner.singleuser_image_spec = os.environ['SINGLEUSER_IMAGE']
68 
69 c.KubeSpawner.singleuser_extra_labels = get_config('singleuser.extra-labels', {})
70 
71 c.KubeSpawner.singleuser_uid = get_config('singleuser.uid')
72 c.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')
73 
74 # Configure dynamically provisioning pvc
75 storage_type = get_config('singleuser.storage.type')
76 if storage_type == 'dynamic':
77     c.KubeSpawner.pvc_name_template = 'claim-{username}{servername}'
78     c.KubeSpawner.user_storage_pvc_ensure = True
79     storage_class = get_config('singleuser.storage.dynamic.storage-class', None)
80     if storage_class:
81         c.KubeSpawner.user_storage_class = storage_class
82     c.KubeSpawner.user_storage_access_modes = ['ReadWriteOnce']
83     c.KubeSpawner.user_storage_capacity = get_config('singleuser.storage.capacity')
84 
85     # Add volumes to singleuser pods
86     c.KubeSpawner.volumes = [
87         {
88             'name': 'volume-{username}{servername}',
89             'persistentVolumeClaim': {
90                 'claimName': 'claim-{username}{servername}'
91             }
92         }
93     ]
94     c.KubeSpawner.volume_mounts = [
95         {
96             'mountPath': get_config('singleuser.storage.home_mount_path'),
97             'name': 'volume-{username}{servername}'
98         }
99     ]
100 elif storage_type == 'static':
101     pvc_claim_name = get_config('singleuser.storage.static.pvc-name')
102     c.KubeSpawner.volumes = [{
103         'name': 'home',
104         'persistentVolumeClaim': {
105             'claimName': pvc_claim_name
106         }
107     }]
108 
109     c.KubeSpawner.volume_mounts = [{
110         'mountPath': get_config('singleuser.storage.home_mount_path'),
111         'name': 'home',
112         'subPath': get_config('singleuser.storage.static.sub-path')
113     }]
114 
115 c.KubeSpawner.volumes.extend(get_config('singleuser.storage.extra-volumes', []))
116 c.KubeSpawner.volume_mounts.extend(get_config('singleuser.storage.extra-volume-mounts', []))
117 
118 lifecycle_hooks = get_config('singleuser.lifecycle-hooks')
119 if lifecycle_hooks:
120     c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks
121 
122 init_containers = get_config('singleuser.init-containers')
123 if init_containers:
124     c.KubeSpawner.singleuser_init_containers = init_containers    
125 
126 # Gives spawned containers access to the API of the hub
127 c.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']
128 c.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])
129 
130 c.JupyterHub.hub_connect_ip = os.environ['HUB_SERVICE_HOST']
131 c.JupyterHub.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])
132 
133 c.KubeSpawner.mem_limit = get_config('singleuser.memory.limit')
134 c.KubeSpawner.mem_guarantee = get_config('singleuser.memory.guarantee')
135 c.KubeSpawner.cpu_limit = get_config('singleuser.cpu.limit')
136 c.KubeSpawner.cpu_guarantee = get_config('singleuser.cpu.guarantee')
137 
138 # Allow switching authenticators easily
139 auth_type = get_config('auth.type')
140 email_domain = 'local'
141 
142 if auth_type == 'google':
143     c.JupyterHub.authenticator_class = 'oauthenticator.GoogleOAuthenticator'
144     c.GoogleOAuthenticator.client_id = get_config('auth.google.client-id')
145     c.GoogleOAuthenticator.client_secret = get_config('auth.google.client-secret')
146     c.GoogleOAuthenticator.oauth_callback_url = get_config('auth.google.callback-url')
147     c.GoogleOAuthenticator.hosted_domain = get_config('auth.google.hosted-domain')
148     c.GoogleOAuthenticator.login_service = get_config('auth.google.login-service')
149     email_domain = get_config('auth.google.hosted-domain')
150 elif auth_type == 'github':
151     c.JupyterHub.authenticator_class = 'oauthenticator.GitHubOAuthenticator'
152     c.GitHubOAuthenticator.oauth_callback_url = get_config('auth.github.callback-url')
153     c.GitHubOAuthenticator.client_id = get_config('auth.github.client-id')
154     c.GitHubOAuthenticator.client_secret = get_config('auth.github.client-secret')
155 elif auth_type == 'cilogon':
156     c.JupyterHub.authenticator_class = 'oauthenticator.CILogonOAuthenticator'
157     c.CILogonOAuthenticator.oauth_callback_url = get_config('auth.cilogon.callback-url')
158     c.CILogonOAuthenticator.client_id = get_config('auth.cilogon.client-id')
159     c.CILogonOAuthenticator.client_secret = get_config('auth.cilogon.client-secret')
160 elif auth_type == 'gitlab':
161     c.JupyterHub.authenticator_class = 'oauthenticator.gitlab.GitLabOAuthenticator'
162     c.GitLabOAuthenticator.oauth_callback_url = get_config('auth.gitlab.callback-url')
163     c.GitLabOAuthenticator.client_id = get_config('auth.gitlab.client-id')
164     c.GitLabOAuthenticator.client_secret = get_config('auth.gitlab.client-secret')
165 elif auth_type == 'mediawiki':
166     c.JupyterHub.authenticator_class = 'oauthenticator.mediawiki.MWOAuthenticator'
167     c.MWOAuthenticator.client_id = get_config('auth.mediawiki.client-id')
168     c.MWOAuthenticator.client_secret = get_config('auth.mediawiki.client-secret')
169     c.MWOAuthenticator.index_url = get_config('auth.mediawiki.index-url')
170 elif auth_type == 'hmac':
171     c.JupyterHub.authenticator_class = 'hmacauthenticator.HMACAuthenticator'
172     c.HMACAuthenticator.secret_key = bytes.fromhex(get_config('auth.hmac.secret-key'))
173 elif auth_type == 'dummy':
174     c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
175     c.DummyAuthenticator.password = get_config('auth.dummy.password', None)
176 elif auth_type == 'tmp':
177     c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'
178 elif auth_type == 'custom':
179     # full_class_name looks like "myauthenticator.MyAuthenticator".
180     # To create a docker image with this class availabe, you can just have the
181     # following Dockerifle:
182     #   FROM jupyterhub/k8s-hub:v0.4
183     #   RUN pip3 install myauthenticator
184     full_class_name = get_config('auth.custom.class-name')
185     c.JupyterHub.authenticator_class = full_class_name
186     auth_class_name = full_class_name.rsplit('.', 1)[-1]
187     auth_config = c[auth_class_name]
188     auth_config.update(get_config('auth.custom.config') or {})
189 else:
190     raise ValueError("Unhandled auth type: %r" % auth_type)
191 
192 
193 def generate_user_email(spawner):
194     """
195     Used as the EMAIL environment variable
196     """
197     return '{username}@{domain}'.format(
198         username=spawner.user.name, domain=email_domain
199     )
200 
201 def generate_user_name(spawner):
202     """
203     Used as GIT_AUTHOR_NAME and GIT_COMMITTER_NAME environment variables
204     """
205     return spawner.user.name
206 
207 c.KubeSpawner.environment = {
208     'EMAIL': generate_user_email,
209     # git requires these committer attributes
210     'GIT_AUTHOR_NAME': generate_user_name,
211     'GIT_COMMITTER_NAME': generate_user_name
212 }
213 
214 c.KubeSpawner.environment.update(get_config('singleuser.extra-env', {}))
215 
216 # Enable admins to access user servers
217 c.JupyterHub.admin_access = get_config('auth.admin.access')
218 
219 
220 c.Authenticator.admin_users = get_config('auth.admin.users', [])
221 
222 c.Authenticator.whitelist = get_config('auth.whitelist.users', [])
223 
224 c.JupyterHub.services = []
225 
226 if get_config('cull.enabled', False):
227     cull_timeout = get_config('cull.timeout')
228     cull_every = get_config('cull.every')
229     cull_cmd = [
230         '/usr/local/bin/cull_idle_servers.py',
231         '--timeout=%s' % cull_timeout,
232         '--cull-every=%s' % cull_every,
233     ]
234     if get_config('cull.users'):
235         cull_cmd.append('--cull-users')
236     c.JupyterHub.services.append({
237         'name': 'cull-idle',
238         'admin': True,
239         'command': cull_cmd,
240     })
241 
242 for name, service in get_config('hub.services', {}).items():
243     api_token = get_secret('services.token.%s' % name)
244     # jupyterhub.services is a list of dicts, but
245     # in the helm chart it is a dict of dicts for easier merged-config
246     service.setdefault('name', name)
247     if api_token:
248         service['api_token'] = api_token
249     c.JupyterHub.services.append(service)
250 
251 c.JupyterHub.base_url = get_config('hub.base_url')
252 
253 c.JupyterHub.db_url = get_config('hub.db_url')
254 
255 cmd = get_config('singleuser.cmd', None)
256 if cmd:
257     c.Spawner.cmd = cmd
258 
259 
260 extra_config_path = '/etc/jupyterhub/config/hub.extra-config.py'
261 if os.path.exists(extra_config_path):
262     load_subconfig(extra_config_path)
263
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/images/hub/jupyterhub_config.py b/images/hub/jupyterhub_config.py
--- a/images/hub/jupyterhub_config.py
+++ b/images/hub/jupyterhub_config.py
@@ -71,6 +71,7 @@
 c.KubeSpawner.singleuser_uid = get_config('singleuser.uid')
 c.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')
 
+c.KubeSpawner.singleuser_node_selector = get_config('singleuser.node-selector')
 # Configure dynamically provisioning pvc
 storage_type = get_config('singleuser.storage.type')
 if storage_type == 'dynamic':

{"golden_diff": "diff --git a/images/hub/jupyterhub_config.py b/images/hub/jupyterhub_config.py\n--- a/images/hub/jupyterhub_config.py\n+++ b/images/hub/jupyterhub_config.py\n@@ -71,6 +71,7 @@\n c.KubeSpawner.singleuser_uid = get_config('singleuser.uid')\n c.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')\n \n+c.KubeSpawner.singleuser_node_selector = get_config('singleuser.node-selector')\n # Configure dynamically provisioning pvc\n storage_type = get_config('singleuser.storage.type')\n if storage_type == 'dynamic':\n", "issue": "nodeSelector support\nI'd like to use node pools on GKE to have a cheaper nodes like `n1-standard-1` host hub related pods and a more expensive nodes host user pods. Is there support for `nodeSelect` pod configuration for the hub and singleuser pods?\r\n\r\nhttps://kubernetes.io/docs/concepts/configuration/assign-pod-node/\r\n\r\nMy use case is a cluster for long term student projects over the course of the semester, and I'd like to use autoscaler on the expensive node pool to go make it go to zero noes when no students are using the cluster, while leaving the hub up on something cheaper while waiting for a user to log in.\n", "before_files": [{"content": "import os\nimport sys\nimport yaml\nfrom tornado.httpclient import AsyncHTTPClient\n\ndef get_config(key, default=None):\n    \"\"\"\n    Find a config item of a given name & return it\n\n    Parses everything as YAML, so lists and dicts are available too\n    \"\"\"\n    path = os.path.join('/etc/jupyterhub/config', key)\n    try:\n        with open(path) as f:\n            data = yaml.safe_load(f)\n            print(key, data)\n            return data\n    except FileNotFoundError:\n        return default\n\ndef get_secret(key, default=None):\n    \"\"\"Get a secret from /etc/jupyterhub/secret\"\"\"\n    path = os.path.join('/etc/jupyterhub/secret', key)\n    try:\n        with open(path) as f:\n            return f.read().strip()\n    except FileNotFoundError:\n        return default\n\n\n# Configure JupyterHub to use the curl backend for making HTTP requests,\n# rather than the pure-python implementations. The default one starts\n# being too slow to make a large number of requests to the proxy API\n# at the rate required.\nAsyncHTTPClient.configure(\"tornado.curl_httpclient.CurlAsyncHTTPClient\")\n\nc.JupyterHub.spawner_class = 'kubespawner.KubeSpawner'\n\n# Connect to a proxy running in a different pod\nc.ConfigurableHTTPProxy.api_url = 'http://{}:{}'.format(os.environ['PROXY_API_SERVICE_HOST'], int(os.environ['PROXY_API_SERVICE_PORT']))\nc.ConfigurableHTTPProxy.should_start = False\n\n# Check that the proxy has routes appropriately setup\n# This isn't the best named setting :D\nc.JupyterHub.last_activity_interval = 60\n\n# Max number of servers that can be spawning at any one time\nc.JupyterHub.concurrent_spawn_limit = get_config('hub.concurrent-spawn-limit')\n\nactive_server_limit = get_config('hub.active-server-limit', None)\n\nif active_server_limit is not None:\n    c.JupyterHub.active_server_limit = int(active_server_limit)\n\nc.JupyterHub.ip = os.environ['PROXY_PUBLIC_SERVICE_HOST']\nc.JupyterHub.port = int(os.environ['PROXY_PUBLIC_SERVICE_PORT'])\n\n# the hub should listen on all interfaces, so the proxy can access it\nc.JupyterHub.hub_ip = '0.0.0.0'\n\nc.KubeSpawner.namespace = os.environ.get('POD_NAMESPACE', 'default')\n\n# Sometimes disks take a while to attach, so let's keep a not-too-short timeout\nc.KubeSpawner.start_timeout = 5 * 60\n\n# Use env var for this, since we want hub to restart when this changes\nc.KubeSpawner.singleuser_image_spec = os.environ['SINGLEUSER_IMAGE']\n\nc.KubeSpawner.singleuser_extra_labels = get_config('singleuser.extra-labels', {})\n\nc.KubeSpawner.singleuser_uid = get_config('singleuser.uid')\nc.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')\n\n# Configure dynamically provisioning pvc\nstorage_type = get_config('singleuser.storage.type')\nif storage_type == 'dynamic':\n    c.KubeSpawner.pvc_name_template = 'claim-{username}{servername}'\n    c.KubeSpawner.user_storage_pvc_ensure = True\n    storage_class = get_config('singleuser.storage.dynamic.storage-class', None)\n    if storage_class:\n        c.KubeSpawner.user_storage_class = storage_class\n    c.KubeSpawner.user_storage_access_modes = ['ReadWriteOnce']\n    c.KubeSpawner.user_storage_capacity = get_config('singleuser.storage.capacity')\n\n    # Add volumes to singleuser pods\n    c.KubeSpawner.volumes = [\n        {\n            'name': 'volume-{username}{servername}',\n            'persistentVolumeClaim': {\n                'claimName': 'claim-{username}{servername}'\n            }\n        }\n    ]\n    c.KubeSpawner.volume_mounts = [\n        {\n            'mountPath': get_config('singleuser.storage.home_mount_path'),\n            'name': 'volume-{username}{servername}'\n        }\n    ]\nelif storage_type == 'static':\n    pvc_claim_name = get_config('singleuser.storage.static.pvc-name')\n    c.KubeSpawner.volumes = [{\n        'name': 'home',\n        'persistentVolumeClaim': {\n            'claimName': pvc_claim_name\n        }\n    }]\n\n    c.KubeSpawner.volume_mounts = [{\n        'mountPath': get_config('singleuser.storage.home_mount_path'),\n        'name': 'home',\n        'subPath': get_config('singleuser.storage.static.sub-path')\n    }]\n\nc.KubeSpawner.volumes.extend(get_config('singleuser.storage.extra-volumes', []))\nc.KubeSpawner.volume_mounts.extend(get_config('singleuser.storage.extra-volume-mounts', []))\n\nlifecycle_hooks = get_config('singleuser.lifecycle-hooks')\nif lifecycle_hooks:\n    c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks\n\ninit_containers = get_config('singleuser.init-containers')\nif init_containers:\n    c.KubeSpawner.singleuser_init_containers = init_containers    \n\n# Gives spawned containers access to the API of the hub\nc.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']\nc.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])\n\nc.JupyterHub.hub_connect_ip = os.environ['HUB_SERVICE_HOST']\nc.JupyterHub.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])\n\nc.KubeSpawner.mem_limit = get_config('singleuser.memory.limit')\nc.KubeSpawner.mem_guarantee = get_config('singleuser.memory.guarantee')\nc.KubeSpawner.cpu_limit = get_config('singleuser.cpu.limit')\nc.KubeSpawner.cpu_guarantee = get_config('singleuser.cpu.guarantee')\n\n# Allow switching authenticators easily\nauth_type = get_config('auth.type')\nemail_domain = 'local'\n\nif auth_type == 'google':\n    c.JupyterHub.authenticator_class = 'oauthenticator.GoogleOAuthenticator'\n    c.GoogleOAuthenticator.client_id = get_config('auth.google.client-id')\n    c.GoogleOAuthenticator.client_secret = get_config('auth.google.client-secret')\n    c.GoogleOAuthenticator.oauth_callback_url = get_config('auth.google.callback-url')\n    c.GoogleOAuthenticator.hosted_domain = get_config('auth.google.hosted-domain')\n    c.GoogleOAuthenticator.login_service = get_config('auth.google.login-service')\n    email_domain = get_config('auth.google.hosted-domain')\nelif auth_type == 'github':\n    c.JupyterHub.authenticator_class = 'oauthenticator.GitHubOAuthenticator'\n    c.GitHubOAuthenticator.oauth_callback_url = get_config('auth.github.callback-url')\n    c.GitHubOAuthenticator.client_id = get_config('auth.github.client-id')\n    c.GitHubOAuthenticator.client_secret = get_config('auth.github.client-secret')\nelif auth_type == 'cilogon':\n    c.JupyterHub.authenticator_class = 'oauthenticator.CILogonOAuthenticator'\n    c.CILogonOAuthenticator.oauth_callback_url = get_config('auth.cilogon.callback-url')\n    c.CILogonOAuthenticator.client_id = get_config('auth.cilogon.client-id')\n    c.CILogonOAuthenticator.client_secret = get_config('auth.cilogon.client-secret')\nelif auth_type == 'gitlab':\n    c.JupyterHub.authenticator_class = 'oauthenticator.gitlab.GitLabOAuthenticator'\n    c.GitLabOAuthenticator.oauth_callback_url = get_config('auth.gitlab.callback-url')\n    c.GitLabOAuthenticator.client_id = get_config('auth.gitlab.client-id')\n    c.GitLabOAuthenticator.client_secret = get_config('auth.gitlab.client-secret')\nelif auth_type == 'mediawiki':\n    c.JupyterHub.authenticator_class = 'oauthenticator.mediawiki.MWOAuthenticator'\n    c.MWOAuthenticator.client_id = get_config('auth.mediawiki.client-id')\n    c.MWOAuthenticator.client_secret = get_config('auth.mediawiki.client-secret')\n    c.MWOAuthenticator.index_url = get_config('auth.mediawiki.index-url')\nelif auth_type == 'hmac':\n    c.JupyterHub.authenticator_class = 'hmacauthenticator.HMACAuthenticator'\n    c.HMACAuthenticator.secret_key = bytes.fromhex(get_config('auth.hmac.secret-key'))\nelif auth_type == 'dummy':\n    c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'\n    c.DummyAuthenticator.password = get_config('auth.dummy.password', None)\nelif auth_type == 'tmp':\n    c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'\nelif auth_type == 'custom':\n    # full_class_name looks like \"myauthenticator.MyAuthenticator\".\n    # To create a docker image with this class availabe, you can just have the\n    # following Dockerifle:\n    #   FROM jupyterhub/k8s-hub:v0.4\n    #   RUN pip3 install myauthenticator\n    full_class_name = get_config('auth.custom.class-name')\n    c.JupyterHub.authenticator_class = full_class_name\n    auth_class_name = full_class_name.rsplit('.', 1)[-1]\n    auth_config = c[auth_class_name]\n    auth_config.update(get_config('auth.custom.config') or {})\nelse:\n    raise ValueError(\"Unhandled auth type: %r\" % auth_type)\n\n\ndef generate_user_email(spawner):\n    \"\"\"\n    Used as the EMAIL environment variable\n    \"\"\"\n    return '{username}@{domain}'.format(\n        username=spawner.user.name, domain=email_domain\n    )\n\ndef generate_user_name(spawner):\n    \"\"\"\n    Used as GIT_AUTHOR_NAME and GIT_COMMITTER_NAME environment variables\n    \"\"\"\n    return spawner.user.name\n\nc.KubeSpawner.environment = {\n    'EMAIL': generate_user_email,\n    # git requires these committer attributes\n    'GIT_AUTHOR_NAME': generate_user_name,\n    'GIT_COMMITTER_NAME': generate_user_name\n}\n\nc.KubeSpawner.environment.update(get_config('singleuser.extra-env', {}))\n\n# Enable admins to access user servers\nc.JupyterHub.admin_access = get_config('auth.admin.access')\n\n\nc.Authenticator.admin_users = get_config('auth.admin.users', [])\n\nc.Authenticator.whitelist = get_config('auth.whitelist.users', [])\n\nc.JupyterHub.services = []\n\nif get_config('cull.enabled', False):\n    cull_timeout = get_config('cull.timeout')\n    cull_every = get_config('cull.every')\n    cull_cmd = [\n        '/usr/local/bin/cull_idle_servers.py',\n        '--timeout=%s' % cull_timeout,\n        '--cull-every=%s' % cull_every,\n    ]\n    if get_config('cull.users'):\n        cull_cmd.append('--cull-users')\n    c.JupyterHub.services.append({\n        'name': 'cull-idle',\n        'admin': True,\n        'command': cull_cmd,\n    })\n\nfor name, service in get_config('hub.services', {}).items():\n    api_token = get_secret('services.token.%s' % name)\n    # jupyterhub.services is a list of dicts, but\n    # in the helm chart it is a dict of dicts for easier merged-config\n    service.setdefault('name', name)\n    if api_token:\n        service['api_token'] = api_token\n    c.JupyterHub.services.append(service)\n\nc.JupyterHub.base_url = get_config('hub.base_url')\n\nc.JupyterHub.db_url = get_config('hub.db_url')\n\ncmd = get_config('singleuser.cmd', None)\nif cmd:\n    c.Spawner.cmd = cmd\n\n\nextra_config_path = '/etc/jupyterhub/config/hub.extra-config.py'\nif os.path.exists(extra_config_path):\n    load_subconfig(extra_config_path)\n", "path": "images/hub/jupyterhub_config.py"}], "after_files": [{"content": "import os\nimport sys\nimport yaml\nfrom tornado.httpclient import AsyncHTTPClient\n\ndef get_config(key, default=None):\n    \"\"\"\n    Find a config item of a given name & return it\n\n    Parses everything as YAML, so lists and dicts are available too\n    \"\"\"\n    path = os.path.join('/etc/jupyterhub/config', key)\n    try:\n        with open(path) as f:\n            data = yaml.safe_load(f)\n            print(key, data)\n            return data\n    except FileNotFoundError:\n        return default\n\ndef get_secret(key, default=None):\n    \"\"\"Get a secret from /etc/jupyterhub/secret\"\"\"\n    path = os.path.join('/etc/jupyterhub/secret', key)\n    try:\n        with open(path) as f:\n            return f.read().strip()\n    except FileNotFoundError:\n        return default\n\n\n# Configure JupyterHub to use the curl backend for making HTTP requests,\n# rather than the pure-python implementations. The default one starts\n# being too slow to make a large number of requests to the proxy API\n# at the rate required.\nAsyncHTTPClient.configure(\"tornado.curl_httpclient.CurlAsyncHTTPClient\")\n\nc.JupyterHub.spawner_class = 'kubespawner.KubeSpawner'\n\n# Connect to a proxy running in a different pod\nc.ConfigurableHTTPProxy.api_url = 'http://{}:{}'.format(os.environ['PROXY_API_SERVICE_HOST'], int(os.environ['PROXY_API_SERVICE_PORT']))\nc.ConfigurableHTTPProxy.should_start = False\n\n# Check that the proxy has routes appropriately setup\n# This isn't the best named setting :D\nc.JupyterHub.last_activity_interval = 60\n\n# Max number of servers that can be spawning at any one time\nc.JupyterHub.concurrent_spawn_limit = get_config('hub.concurrent-spawn-limit')\n\nactive_server_limit = get_config('hub.active-server-limit', None)\n\nif active_server_limit is not None:\n    c.JupyterHub.active_server_limit = int(active_server_limit)\n\nc.JupyterHub.ip = os.environ['PROXY_PUBLIC_SERVICE_HOST']\nc.JupyterHub.port = int(os.environ['PROXY_PUBLIC_SERVICE_PORT'])\n\n# the hub should listen on all interfaces, so the proxy can access it\nc.JupyterHub.hub_ip = '0.0.0.0'\n\nc.KubeSpawner.namespace = os.environ.get('POD_NAMESPACE', 'default')\n\n# Sometimes disks take a while to attach, so let's keep a not-too-short timeout\nc.KubeSpawner.start_timeout = 5 * 60\n\n# Use env var for this, since we want hub to restart when this changes\nc.KubeSpawner.singleuser_image_spec = os.environ['SINGLEUSER_IMAGE']\n\nc.KubeSpawner.singleuser_extra_labels = get_config('singleuser.extra-labels', {})\n\nc.KubeSpawner.singleuser_uid = get_config('singleuser.uid')\nc.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')\n\nc.KubeSpawner.singleuser_node_selector = get_config('singleuser.node-selector')\n# Configure dynamically provisioning pvc\nstorage_type = get_config('singleuser.storage.type')\nif storage_type == 'dynamic':\n    c.KubeSpawner.pvc_name_template = 'claim-{username}{servername}'\n    c.KubeSpawner.user_storage_pvc_ensure = True\n    storage_class = get_config('singleuser.storage.dynamic.storage-class', None)\n    if storage_class:\n        c.KubeSpawner.user_storage_class = storage_class\n    c.KubeSpawner.user_storage_access_modes = ['ReadWriteOnce']\n    c.KubeSpawner.user_storage_capacity = get_config('singleuser.storage.capacity')\n\n    # Add volumes to singleuser pods\n    c.KubeSpawner.volumes = [\n        {\n            'name': 'volume-{username}{servername}',\n            'persistentVolumeClaim': {\n                'claimName': 'claim-{username}{servername}'\n            }\n        }\n    ]\n    c.KubeSpawner.volume_mounts = [\n        {\n            'mountPath': get_config('singleuser.storage.home_mount_path'),\n            'name': 'volume-{username}{servername}'\n        }\n    ]\nelif storage_type == 'static':\n    pvc_claim_name = get_config('singleuser.storage.static.pvc-name')\n    c.KubeSpawner.volumes = [{\n        'name': 'home',\n        'persistentVolumeClaim': {\n            'claimName': pvc_claim_name\n        }\n    }]\n\n    c.KubeSpawner.volume_mounts = [{\n        'mountPath': get_config('singleuser.storage.home_mount_path'),\n        'name': 'home',\n        'subPath': get_config('singleuser.storage.static.sub-path')\n    }]\n\nc.KubeSpawner.volumes.extend(get_config('singleuser.storage.extra-volumes', []))\nc.KubeSpawner.volume_mounts.extend(get_config('singleuser.storage.extra-volume-mounts', []))\n\nlifecycle_hooks = get_config('singleuser.lifecycle-hooks')\nif lifecycle_hooks:\n    c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks\n\ninit_containers = get_config('singleuser.init-containers')\nif init_containers:\n    c.KubeSpawner.singleuser_init_containers = init_containers    \n\n# Gives spawned containers access to the API of the hub\nc.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']\nc.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])\n\nc.JupyterHub.hub_connect_ip = os.environ['HUB_SERVICE_HOST']\nc.JupyterHub.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])\n\nc.KubeSpawner.mem_limit = get_config('singleuser.memory.limit')\nc.KubeSpawner.mem_guarantee = get_config('singleuser.memory.guarantee')\nc.KubeSpawner.cpu_limit = get_config('singleuser.cpu.limit')\nc.KubeSpawner.cpu_guarantee = get_config('singleuser.cpu.guarantee')\n\n# Allow switching authenticators easily\nauth_type = get_config('auth.type')\nemail_domain = 'local'\n\nif auth_type == 'google':\n    c.JupyterHub.authenticator_class = 'oauthenticator.GoogleOAuthenticator'\n    c.GoogleOAuthenticator.client_id = get_config('auth.google.client-id')\n    c.GoogleOAuthenticator.client_secret = get_config('auth.google.client-secret')\n    c.GoogleOAuthenticator.oauth_callback_url = get_config('auth.google.callback-url')\n    c.GoogleOAuthenticator.hosted_domain = get_config('auth.google.hosted-domain')\n    c.GoogleOAuthenticator.login_service = get_config('auth.google.login-service')\n    email_domain = get_config('auth.google.hosted-domain')\nelif auth_type == 'github':\n    c.JupyterHub.authenticator_class = 'oauthenticator.GitHubOAuthenticator'\n    c.GitHubOAuthenticator.oauth_callback_url = get_config('auth.github.callback-url')\n    c.GitHubOAuthenticator.client_id = get_config('auth.github.client-id')\n    c.GitHubOAuthenticator.client_secret = get_config('auth.github.client-secret')\nelif auth_type == 'cilogon':\n    c.JupyterHub.authenticator_class = 'oauthenticator.CILogonOAuthenticator'\n    c.CILogonOAuthenticator.oauth_callback_url = get_config('auth.cilogon.callback-url')\n    c.CILogonOAuthenticator.client_id = get_config('auth.cilogon.client-id')\n    c.CILogonOAuthenticator.client_secret = get_config('auth.cilogon.client-secret')\nelif auth_type == 'gitlab':\n    c.JupyterHub.authenticator_class = 'oauthenticator.gitlab.GitLabOAuthenticator'\n    c.GitLabOAuthenticator.oauth_callback_url = get_config('auth.gitlab.callback-url')\n    c.GitLabOAuthenticator.client_id = get_config('auth.gitlab.client-id')\n    c.GitLabOAuthenticator.client_secret = get_config('auth.gitlab.client-secret')\nelif auth_type == 'mediawiki':\n    c.JupyterHub.authenticator_class = 'oauthenticator.mediawiki.MWOAuthenticator'\n    c.MWOAuthenticator.client_id = get_config('auth.mediawiki.client-id')\n    c.MWOAuthenticator.client_secret = get_config('auth.mediawiki.client-secret')\n    c.MWOAuthenticator.index_url = get_config('auth.mediawiki.index-url')\nelif auth_type == 'hmac':\n    c.JupyterHub.authenticator_class = 'hmacauthenticator.HMACAuthenticator'\n    c.HMACAuthenticator.secret_key = bytes.fromhex(get_config('auth.hmac.secret-key'))\nelif auth_type == 'dummy':\n    c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'\n    c.DummyAuthenticator.password = get_config('auth.dummy.password', None)\nelif auth_type == 'tmp':\n    c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'\nelif auth_type == 'custom':\n    # full_class_name looks like \"myauthenticator.MyAuthenticator\".\n    # To create a docker image with this class availabe, you can just have the\n    # following Dockerifle:\n    #   FROM jupyterhub/k8s-hub:v0.4\n    #   RUN pip3 install myauthenticator\n    full_class_name = get_config('auth.custom.class-name')\n    c.JupyterHub.authenticator_class = full_class_name\n    auth_class_name = full_class_name.rsplit('.', 1)[-1]\n    auth_config = c[auth_class_name]\n    auth_config.update(get_config('auth.custom.config') or {})\nelse:\n    raise ValueError(\"Unhandled auth type: %r\" % auth_type)\n\n\ndef generate_user_email(spawner):\n    \"\"\"\n    Used as the EMAIL environment variable\n    \"\"\"\n    return '{username}@{domain}'.format(\n        username=spawner.user.name, domain=email_domain\n    )\n\ndef generate_user_name(spawner):\n    \"\"\"\n    Used as GIT_AUTHOR_NAME and GIT_COMMITTER_NAME environment variables\n    \"\"\"\n    return spawner.user.name\n\nc.KubeSpawner.environment = {\n    'EMAIL': generate_user_email,\n    # git requires these committer attributes\n    'GIT_AUTHOR_NAME': generate_user_name,\n    'GIT_COMMITTER_NAME': generate_user_name\n}\n\nc.KubeSpawner.environment.update(get_config('singleuser.extra-env', {}))\n\n# Enable admins to access user servers\nc.JupyterHub.admin_access = get_config('auth.admin.access')\n\n\nc.Authenticator.admin_users = get_config('auth.admin.users', [])\n\nc.Authenticator.whitelist = get_config('auth.whitelist.users', [])\n\nc.JupyterHub.services = []\n\nif get_config('cull.enabled', False):\n    cull_timeout = get_config('cull.timeout')\n    cull_every = get_config('cull.every')\n    cull_cmd = [\n        '/usr/local/bin/cull_idle_servers.py',\n        '--timeout=%s' % cull_timeout,\n        '--cull-every=%s' % cull_every,\n    ]\n    if get_config('cull.users'):\n        cull_cmd.append('--cull-users')\n    c.JupyterHub.services.append({\n        'name': 'cull-idle',\n        'admin': True,\n        'command': cull_cmd,\n    })\n\nfor name, service in get_config('hub.services', {}).items():\n    api_token = get_secret('services.token.%s' % name)\n    # jupyterhub.services is a list of dicts, but\n    # in the helm chart it is a dict of dicts for easier merged-config\n    service.setdefault('name', name)\n    if api_token:\n        service['api_token'] = api_token\n    c.JupyterHub.services.append(service)\n\nc.JupyterHub.base_url = get_config('hub.base_url')\n\nc.JupyterHub.db_url = get_config('hub.db_url')\n\ncmd = get_config('singleuser.cmd', None)\nif cmd:\n    c.Spawner.cmd = cmd\n\n\nextra_config_path = '/etc/jupyterhub/config/hub.extra-config.py'\nif os.path.exists(extra_config_path):\n    load_subconfig(extra_config_path)\n", "path": "images/hub/jupyterhub_config.py"}]}