We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow multiple extraConfig
Binder relies on `hub.extraConfig` to configure the Spawner. If we want to customize a given Binder *deployment* via `hub.extraConfig`, we have to copy/paste the hub.extraConfig from the binderhub chart and add to it.

It would be handy if extraConfig were something that could be added to without having to copy the previous value. Are dicts the only structure that can do this?

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

{"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@@ -1,4 +1,5 @@\n import os\n+import glob\n import sys\n import yaml\n from tornado.httpclient import AsyncHTTPClient\n@@ -295,6 +296,6 @@\n     # Set default to {} so subconfigs can easily update it\n     c.KubeSpawner.singleuser_extra_pod_config = {}\n \n-extra_config_path = '/etc/jupyterhub/config/hub.extra-config.py'\n-if os.path.exists(extra_config_path):\n-    load_subconfig(extra_config_path)\n+extra_configs = sorted(glob.glob('/etc/jupyterhub/config/hub.extra-config.*.py'))\n+for ec in extra_configs:\n+    load_subconfig(ec)\n", "issue": "Allow multiple extraConfig\nBinder relies on `hub.extraConfig` to configure the Spawner. If we want to customize a given Binder *deployment* via `hub.extraConfig`, we have to copy/paste the hub.extraConfig from the binderhub chart and add to it.\r\n\r\nIt would be handy if extraConfig were something that could be added to without having to copy the previous value. Are dicts the only structure that can do this?\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            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# Do not shut down user pods when hub is restarted\nc.JupyterHub.cleanup_servers = 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\nc.KubeSpawner.start_timeout = get_config('singleuser.start-timeout')\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 == 'globus':\n    c.JupyterHub.authenticator_class = 'oauthenticator.globus.GlobusOAuthenticator'\n    c.GlobusOAuthenticator.oauth_callback_url = get_config('auth.globus.callback-url')\n    c.GlobusOAuthenticator.client_id = get_config('auth.globus.client-id')\n    c.GlobusOAuthenticator.client_secret = get_config('auth.globus.client-secret')\n    c.GlobusOAuthenticator.identity_provider = get_config('auth.globus.identity-provider', '')\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 == 'lti':\n    c.JupyterHub.authenticator_class = 'ltiauthenticator.LTIAuthenticator'\n    c.LTIAuthenticator.consumers = get_config('auth.lti.consumers')\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\nc.Authenticator.enable_auth_state = get_config('auth.state.enabled', False)\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')\nc.Authenticator.admin_users = get_config('auth.admin.users', [])\nc.Authenticator.whitelist = get_config('auth.whitelist.users', [])\n\nc.JupyterHub.base_url = get_config('hub.base_url')\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        '--url=http://127.0.0.1:8081' + c.JupyterHub.base_url + 'hub/api'\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\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\nscheduler_strategy = get_config('singleuser.scheduler-strategy', 'spread')\n\nif scheduler_strategy == 'pack':\n    # FIXME: Support setting affinity directly in KubeSpawner\n    c.KubeSpawner.singleuser_extra_pod_config = {\n        'affinity': {\n            'podAffinity': {\n                'preferredDuringSchedulingIgnoredDuringExecution': [{\n                    'weight': 100,\n                    'podAffinityTerm': {\n                        'labelSelector': {\n                            'matchExpressions': [{\n                                'key': 'component',\n                                'operator': 'In',\n                                'values': ['singleuser-server']\n                            }]\n                        },\n                        'topologyKey': 'kubernetes.io/hostname'\n                    }\n                }],\n            }\n        }\n    }\nelse:\n    # Set default to {} so subconfigs can easily update it\n    c.KubeSpawner.singleuser_extra_pod_config = {}\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 glob\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            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# Do not shut down user pods when hub is restarted\nc.JupyterHub.cleanup_servers = 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\nc.KubeSpawner.start_timeout = get_config('singleuser.start-timeout')\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 == 'globus':\n    c.JupyterHub.authenticator_class = 'oauthenticator.globus.GlobusOAuthenticator'\n    c.GlobusOAuthenticator.oauth_callback_url = get_config('auth.globus.callback-url')\n    c.GlobusOAuthenticator.client_id = get_config('auth.globus.client-id')\n    c.GlobusOAuthenticator.client_secret = get_config('auth.globus.client-secret')\n    c.GlobusOAuthenticator.identity_provider = get_config('auth.globus.identity-provider', '')\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 == 'lti':\n    c.JupyterHub.authenticator_class = 'ltiauthenticator.LTIAuthenticator'\n    c.LTIAuthenticator.consumers = get_config('auth.lti.consumers')\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\nc.Authenticator.enable_auth_state = get_config('auth.state.enabled', False)\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')\nc.Authenticator.admin_users = get_config('auth.admin.users', [])\nc.Authenticator.whitelist = get_config('auth.whitelist.users', [])\n\nc.JupyterHub.base_url = get_config('hub.base_url')\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        '--url=http://127.0.0.1:8081' + c.JupyterHub.base_url + 'hub/api'\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\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\nscheduler_strategy = get_config('singleuser.scheduler-strategy', 'spread')\n\nif scheduler_strategy == 'pack':\n    # FIXME: Support setting affinity directly in KubeSpawner\n    c.KubeSpawner.singleuser_extra_pod_config = {\n        'affinity': {\n            'podAffinity': {\n                'preferredDuringSchedulingIgnoredDuringExecution': [{\n                    'weight': 100,\n                    'podAffinityTerm': {\n                        'labelSelector': {\n                            'matchExpressions': [{\n                                'key': 'component',\n                                'operator': 'In',\n                                'values': ['singleuser-server']\n                            }]\n                        },\n                        'topologyKey': 'kubernetes.io/hostname'\n                    }\n                }],\n            }\n        }\n    }\nelse:\n    # Set default to {} so subconfigs can easily update it\n    c.KubeSpawner.singleuser_extra_pod_config = {}\n\nextra_configs = sorted(glob.glob('/etc/jupyterhub/config/hub.extra-config.*.py'))\nfor ec in extra_configs:\n    load_subconfig(ec)\n", "path": "images/hub/jupyterhub_config.py"}]}