We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
add handling of init_containers to the default jupyterhub_config.py
While experimenting a bit with lifecycle hooks and init containers with kubespawner, I noticed that while lifecycle hooks are captured in the jupyterhub_config.py [here](https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/master/images/hub/jupyterhub_config.py#L111-L113), there's not out-of-the-box handling for init-containers.

Does it make sense to add this to jupyterhub_config.py?
```
init_containers = get_config('singleuser.init-containers')
if init_containers:
    c.KubeSpawner.singleuser_init_containers = init_containers
```
Let me know and I can send a PR.

--- 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 c.JupyterHub.ip = os.environ['PROXY_PUBLIC_SERVICE_HOST']
51 c.JupyterHub.port = int(os.environ['PROXY_PUBLIC_SERVICE_PORT'])
52 
53 # the hub should listen on all interfaces, so the proxy can access it
54 c.JupyterHub.hub_ip = '0.0.0.0'
55 
56 c.KubeSpawner.namespace = os.environ.get('POD_NAMESPACE', 'default')
57 
58 # Sometimes disks take a while to attach, so let's keep a not-too-short timeout
59 c.KubeSpawner.start_timeout = 5 * 60
60 
61 # Use env var for this, since we want hub to restart when this changes
62 c.KubeSpawner.singleuser_image_spec = os.environ['SINGLEUSER_IMAGE']
63 
64 c.KubeSpawner.singleuser_extra_labels = get_config('singleuser.extra-labels', {})
65 
66 c.KubeSpawner.singleuser_uid = get_config('singleuser.uid')
67 c.KubeSpawner.singleuser_fs_gid = get_config('singleuser.fs-gid')
68 
69 # Configure dynamically provisioning pvc
70 storage_type = get_config('singleuser.storage.type')
71 if storage_type == 'dynamic':
72     c.KubeSpawner.pvc_name_template = 'claim-{username}{servername}'
73     c.KubeSpawner.user_storage_pvc_ensure = True
74     storage_class = get_config('singleuser.storage.dynamic.storage-class', None)
75     if storage_class:
76         c.KubeSpawner.user_storage_class = storage_class
77     c.KubeSpawner.user_storage_access_modes = ['ReadWriteOnce']
78     c.KubeSpawner.user_storage_capacity = get_config('singleuser.storage.capacity')
79 
80     # Add volumes to singleuser pods
81     c.KubeSpawner.volumes = [
82         {
83             'name': 'volume-{username}{servername}',
84             'persistentVolumeClaim': {
85                 'claimName': 'claim-{username}{servername}'
86             }
87         }
88     ]
89     c.KubeSpawner.volume_mounts = [
90         {
91             'mountPath': get_config('singleuser.storage.home_mount_path'),
92             'name': 'volume-{username}{servername}'
93         }
94     ]
95 elif storage_type == 'static':
96     pvc_claim_name = get_config('singleuser.storage.static.pvc-name')
97     c.KubeSpawner.volumes = [{
98         'name': 'home',
99         'persistentVolumeClaim': {
100             'claimName': pvc_claim_name
101         }
102     }]
103 
104     c.KubeSpawner.volume_mounts = [{
105         'mountPath': get_config('singleuser.storage.home_mount_path'),
106         'name': 'home',
107         'subPath': get_config('singleuser.storage.static.sub-path')
108     }]
109 
110 c.KubeSpawner.volumes.extend(get_config('singleuser.storage.extra-volumes', []))
111 c.KubeSpawner.volume_mounts.extend(get_config('singleuser.storage.extra-volume-mounts', []))
112 
113 lifecycle_hooks = get_config('singleuser.lifecycle-hooks')
114 if lifecycle_hooks:
115     c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks
116 
117 # Gives spawned containers access to the API of the hub
118 c.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']
119 c.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])
120 
121 c.JupyterHub.hub_connect_ip = os.environ['HUB_SERVICE_HOST']
122 c.JupyterHub.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])
123 
124 c.KubeSpawner.mem_limit = get_config('singleuser.memory.limit')
125 c.KubeSpawner.mem_guarantee = get_config('singleuser.memory.guarantee')
126 c.KubeSpawner.cpu_limit = get_config('singleuser.cpu.limit')
127 c.KubeSpawner.cpu_guarantee = get_config('singleuser.cpu.guarantee')
128 
129 # Allow switching authenticators easily
130 auth_type = get_config('auth.type')
131 email_domain = 'local'
132 
133 if auth_type == 'google':
134     c.JupyterHub.authenticator_class = 'oauthenticator.GoogleOAuthenticator'
135     c.GoogleOAuthenticator.client_id = get_config('auth.google.client-id')
136     c.GoogleOAuthenticator.client_secret = get_config('auth.google.client-secret')
137     c.GoogleOAuthenticator.oauth_callback_url = get_config('auth.google.callback-url')
138     c.GoogleOAuthenticator.hosted_domain = get_config('auth.google.hosted-domain')
139     c.GoogleOAuthenticator.login_service = get_config('auth.google.login-service')
140     email_domain = get_config('auth.google.hosted-domain')
141 elif auth_type == 'github':
142     c.JupyterHub.authenticator_class = 'oauthenticator.GitHubOAuthenticator'
143     c.GitHubOAuthenticator.oauth_callback_url = get_config('auth.github.callback-url')
144     c.GitHubOAuthenticator.client_id = get_config('auth.github.client-id')
145     c.GitHubOAuthenticator.client_secret = get_config('auth.github.client-secret')
146 elif auth_type == 'cilogon':
147     c.JupyterHub.authenticator_class = 'oauthenticator.CILogonOAuthenticator'
148     c.CILogonOAuthenticator.oauth_callback_url = get_config('auth.cilogon.callback-url')
149     c.CILogonOAuthenticator.client_id = get_config('auth.cilogon.client-id')
150     c.CILogonOAuthenticator.client_secret = get_config('auth.cilogon.client-secret')
151 elif auth_type == 'gitlab':
152     c.JupyterHub.authenticator_class = 'oauthenticator.gitlab.GitLabOAuthenticator'
153     c.GitLabOAuthenticator.oauth_callback_url = get_config('auth.gitlab.callback-url')
154     c.GitLabOAuthenticator.client_id = get_config('auth.gitlab.client-id')
155     c.GitLabOAuthenticator.client_secret = get_config('auth.gitlab.client-secret')
156 elif auth_type == 'mediawiki':
157     c.JupyterHub.authenticator_class = 'oauthenticator.mediawiki.MWOAuthenticator'
158     c.MWOAuthenticator.client_id = get_config('auth.mediawiki.client-id')
159     c.MWOAuthenticator.client_secret = get_config('auth.mediawiki.client-secret')
160     c.MWOAuthenticator.index_url = get_config('auth.mediawiki.index-url')
161 elif auth_type == 'hmac':
162     c.JupyterHub.authenticator_class = 'hmacauthenticator.HMACAuthenticator'
163     c.HMACAuthenticator.secret_key = bytes.fromhex(get_config('auth.hmac.secret-key'))
164 elif auth_type == 'dummy':
165     c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
166     c.DummyAuthenticator.password = get_config('auth.dummy.password', None)
167 elif auth_type == 'tmp':
168     c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator'
169 elif auth_type == 'custom':
170     # full_class_name looks like "myauthenticator.MyAuthenticator".
171     # To create a docker image with this class availabe, you can just have the
172     # following Dockerifle:
173     #   FROM jupyterhub/k8s-hub:v0.4
174     #   RUN pip3 install myauthenticator
175     full_class_name = get_config('auth.custom.class-name')
176     c.JupyterHub.authenticator_class = full_class_name
177     auth_class_name = full_class_name.rsplit('.', 1)[-1]
178     auth_config = c[auth_class_name]
179     auth_config.update(get_config('auth.custom.config') or {})
180 else:
181     raise ValueError("Unhandled auth type: %r" % auth_type)
182 
183 
184 def generate_user_email(spawner):
185     """
186     Used as the EMAIL environment variable
187     """
188     return '{username}@{domain}'.format(
189         username=spawner.user.name, domain=email_domain
190     )
191 
192 def generate_user_name(spawner):
193     """
194     Used as GIT_AUTHOR_NAME and GIT_COMMITTER_NAME environment variables
195     """
196     return spawner.user.name
197 
198 c.KubeSpawner.environment = {
199     'EMAIL': generate_user_email,
200     # git requires these committer attributes
201     'GIT_AUTHOR_NAME': generate_user_name,
202     'GIT_COMMITTER_NAME': generate_user_name
203 }
204 
205 c.KubeSpawner.environment.update(get_config('singleuser.extra-env', {}))
206 
207 # Enable admins to access user servers
208 c.JupyterHub.admin_access = get_config('admin.access')
209 
210 
211 c.Authenticator.admin_users = get_config('admin.users', [])
212 
213 c.Authenticator.whitelist = get_config('hub.whitelist.users', [])
214 
215 c.JupyterHub.services = []
216 
217 if get_config('cull.enabled', False):
218     cull_timeout = get_config('cull.timeout')
219     cull_every = get_config('cull.every')
220     cull_cmd = [
221         '/usr/local/bin/cull_idle_servers.py',
222         '--timeout=%s' % cull_timeout,
223         '--cull-every=%s' % cull_every,
224     ]
225     if get_config('cull.users'):
226         cull_cmd.append('--cull-users')
227     c.JupyterHub.services.append({
228         'name': 'cull-idle',
229         'admin': True,
230         'command': cull_cmd,
231     })
232 
233 for name, service in get_config('hub.services', {}).items():
234     api_token = get_secret('services.token.%s' % name)
235     # jupyterhub.services is a list of dicts, but
236     # in the helm chart it is a dict of dicts for easier merged-config
237     service.setdefault('name', name)
238     if api_token:
239         service['api_token'] = api_token
240     c.JupyterHub.services.append(service)
241 
242 c.JupyterHub.base_url = get_config('hub.base_url')
243 
244 c.JupyterHub.db_url = get_config('hub.db_url')
245 
246 cmd = get_config('singleuser.cmd', None)
247 if cmd:
248     c.Spawner.cmd = cmd
249 
250 
251 extra_config_path = '/etc/jupyterhub/config/hub.extra-config.py'
252 if os.path.exists(extra_config_path):
253     load_subconfig(extra_config_path)
254
```
--- 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
@@ -114,6 +114,10 @@
 if lifecycle_hooks:
     c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks
 
+init_containers = get_config('singleuser.init-containers')
+if init_containers:
+    c.KubeSpawner.singleuser_init_containers = init_containers    
+
 # Gives spawned containers access to the API of the hub
 c.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']
 c.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])

{"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@@ -114,6 +114,10 @@\n if lifecycle_hooks:\n     c.KubeSpawner.singleuser_lifecycle_hooks = lifecycle_hooks\n \n+init_containers = get_config('singleuser.init-containers')\n+if init_containers:\n+    c.KubeSpawner.singleuser_init_containers = init_containers    \n+\n # Gives spawned containers access to the API of the hub\n c.KubeSpawner.hub_connect_ip = os.environ['HUB_SERVICE_HOST']\n c.KubeSpawner.hub_connect_port = int(os.environ['HUB_SERVICE_PORT'])\n", "issue": "add handling of init_containers to the default jupyterhub_config.py\nWhile experimenting a bit with lifecycle hooks and init containers with kubespawner, I noticed that while lifecycle hooks are captured in the jupyterhub_config.py [here](https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/master/images/hub/jupyterhub_config.py#L111-L113), there's not out-of-the-box handling for init-containers.\r\n\r\nDoes it make sense to add this to jupyterhub_config.py?\r\n```\r\ninit_containers = get_config('singleuser.init-containers')\r\nif init_containers:\r\n    c.KubeSpawner.singleuser_init_containers = init_containers\r\n```\r\nLet me know and I can send a PR.\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\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\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('admin.access')\n\n\nc.Authenticator.admin_users = get_config('admin.users', [])\n\nc.Authenticator.whitelist = get_config('hub.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\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('admin.access')\n\n\nc.Authenticator.admin_users = get_config('admin.users', [])\n\nc.Authenticator.whitelist = get_config('hub.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"}]}