-
Notifications
You must be signed in to change notification settings - Fork 303
/
Copy pathspawner.py
2936 lines (2489 loc) · 109 KB
/
spawner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
JupyterHub Spawner to spawn user notebooks on a Kubernetes cluster.
This module exports `KubeSpawner` class, which is the actual spawner
implementation that should be used by JupyterHub.
"""
import asyncio
import multiprocessing
import os
import signal
import string
import sys
import warnings
from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from functools import partial
from urllib.parse import urlparse
import escapism
import kubernetes.config
from jinja2 import BaseLoader
from jinja2 import Environment
from jupyterhub.spawner import Spawner
from jupyterhub.traitlets import Command
from jupyterhub.utils import exponential_backoff
from kubernetes import client
from kubernetes.client.rest import ApiException
from slugify import slugify
from tornado import gen
from tornado.concurrent import run_on_executor
from tornado.ioloop import IOLoop
from traitlets import Bool
from traitlets import default
from traitlets import Dict
from traitlets import Integer
from traitlets import List
from traitlets import observe
from traitlets import Unicode
from traitlets import Union
from traitlets import validate
from .clients import shared_client
from .objects import make_namespace
from .objects import make_owner_reference
from .objects import make_pod
from .objects import make_pvc
from .objects import make_secret
from .objects import make_service
from .reflector import ResourceReflector
from .traitlets import Callable
class PodReflector(ResourceReflector):
"""
PodReflector is merely a configured ResourceReflector. It exposes
the pods property, which is simply mapping to self.resources where the
ResourceReflector keeps an updated list of the resource defined by
the `kind` field and the `list_method_name` field.
"""
kind = "pods"
# The default component label can be over-ridden by specifying the component_label property
labels = {
'component': 'singleuser-server',
}
@property
def pods(self):
"""
A dictionary of pods for the namespace as returned by the Kubernetes
API. The dictionary keys are the pod ids and the values are
dictionaries of the actual pod resource values.
ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#pod-v1-core
"""
return self.resources
class EventReflector(ResourceReflector):
"""
EventsReflector is merely a configured ResourceReflector. It
exposes the events property, which is simply mapping to self.resources where
the ResourceReflector keeps an updated list of the resource
defined by the `kind` field and the `list_method_name` field.
"""
kind = "events"
@property
def events(self):
"""
Returns list of dictionaries representing the k8s
events within the namespace, sorted by the latest event.
ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#event-v1-core
"""
# NOTE:
# - self.resources is a dictionary with keys mapping unique ids of
# Kubernetes Event resources, updated by ResourceReflector.
# self.resources will builds up with incoming k8s events, but can also
# suddenly refreshes itself entirely. We should not assume a call to
# this dictionary's values will result in a consistently ordered list,
# so we sort it to get it somewhat more structured.
# - We either seem to get only event['lastTimestamp'] or
# event['eventTime'], both fields serve the same role but the former
# is a low resolution timestamp without and the other is a higher
# resolution timestamp.
return sorted(
self.resources.values(),
key=lambda event: event["lastTimestamp"] or event["eventTime"],
)
class MockObject(object):
pass
class KubeSpawner(Spawner):
"""
A JupyterHub spawner that spawn pods in a Kubernetes Cluster. Each server
spawned by a user will have its own KubeSpawner instance.
"""
# We want to have one single threadpool executor that is shared across all
# KubeSpawner instances, so we apply a Singleton pattern. We initialize this
# class variable from the first KubeSpawner instance that is created and
# then reference it from all instances. The same goes for the PodReflector
# and EventReflector.
executor = None
reflectors = {
"pods": None,
"events": None,
}
# Characters as defined by safe for DNS
# Note: '-' is not in safe_chars, as it is being used as escape character
safe_chars = set(string.ascii_lowercase + string.digits)
@property
def pod_reflector(self):
"""
A convenience alias to the class variable reflectors['pods'].
"""
return self.__class__.reflectors['pods']
@property
def event_reflector(self):
"""
A convenience alias to the class variable reflectors['events'] if the
spawner instance has events_enabled.
"""
if self.events_enabled:
return self.__class__.reflectors['events']
def __init__(self, *args, **kwargs):
_mock = kwargs.pop('_mock', False)
super().__init__(*args, **kwargs)
if _mock:
# runs during test execution only
if 'user' not in kwargs:
user = MockObject()
user.name = 'mock_name'
user.id = 'mock_id'
user.url = 'mock_url'
self.user = user
if 'hub' not in kwargs:
hub = MockObject()
hub.public_host = 'mock_public_host'
hub.url = 'mock_url'
hub.base_url = 'mock_base_url'
hub.api_url = 'mock_api_url'
self.hub = hub
# We have to set the namespace (if user namespaces are enabled)
# before we start the reflectors, so this must run before
# watcher start in normal execution. We still want to get the
# namespace right for test, though, so we need self.user to have
# been set in order to do that.
# By now, all the traitlets have been set, so we can use them to
# compute other attributes
if self.enable_user_namespaces:
self.namespace = self._expand_user_properties(self.user_namespace_template)
self.log.info("Using user namespace: {}".format(self.namespace))
if not _mock:
# runs during normal execution only
if self.__class__.executor is None:
self.log.debug(
'Starting executor thread pool with %d workers',
self.k8s_api_threadpool_workers,
)
self.__class__.executor = ThreadPoolExecutor(
max_workers=self.k8s_api_threadpool_workers
)
# Set global kubernetes client configurations
# before reflector.py code runs
self._set_k8s_client_configuration()
self.api = shared_client('CoreV1Api')
# This will start watching in __init__, so it'll start the first
# time any spawner object is created. Not ideal but works!
self._start_watching_pods()
if self.events_enabled:
self._start_watching_events()
# runs during both test and normal execution
self.pod_name = self._expand_user_properties(self.pod_name_template)
self.dns_name = self.dns_name_template.format(
namespace=self.namespace, name=self.pod_name
)
self.secret_name = self._expand_user_properties(self.secret_name_template)
self.pvc_name = self._expand_user_properties(self.pvc_name_template)
if self.working_dir:
self.working_dir = self._expand_user_properties(self.working_dir)
if self.port == 0:
# Our default port is 8888
self.port = 8888
def _set_k8s_client_configuration(self):
# The actual (singleton) Kubernetes client will be created
# in clients.py shared_client but the configuration
# for token / ca_cert / k8s api host is set globally
# in kubernetes.py syntax. It is being set here
# and this method called prior to shared_client
# for readability / coupling with traitlets values
try:
kubernetes.config.load_incluster_config()
except kubernetes.config.ConfigException:
kubernetes.config.load_kube_config()
if self.k8s_api_ssl_ca_cert:
global_conf = client.Configuration.get_default_copy()
global_conf.ssl_ca_cert = self.k8s_api_ssl_ca_cert
client.Configuration.set_default(global_conf)
if self.k8s_api_host:
global_conf = client.Configuration.get_default_copy()
global_conf.host = self.k8s_api_host
client.Configuration.set_default(global_conf)
k8s_api_ssl_ca_cert = Unicode(
"",
config=True,
help="""
Location (absolute filepath) for CA certs of the k8s API server.
Typically this is unnecessary, CA certs are picked up by
config.load_incluster_config() or config.load_kube_config.
In rare non-standard cases, such as using custom intermediate CA
for your cluster, you may need to mount root CA's elsewhere in
your Pod/Container and point this variable to that filepath
""",
)
k8s_api_host = Unicode(
"",
config=True,
help="""
Full host name of the k8s API server ("https://hostname:port").
Typically this is unnecessary, the hostname is picked up by
config.load_incluster_config() or config.load_kube_config.
""",
)
k8s_api_threadpool_workers = Integer(
# Set this explicitly, since this is the default in Python 3.5+
# but not in 3.4
5 * multiprocessing.cpu_count(),
config=True,
help="""
Number of threads in thread pool used to talk to the k8s API.
Increase this if you are dealing with a very large number of users.
Defaults to `5 * cpu_cores`, which is the default for `ThreadPoolExecutor`.
""",
)
k8s_api_request_timeout = Integer(
3,
config=True,
help="""
API request timeout (in seconds) for all k8s API calls.
This is the total amount of time a request might take before the connection
is killed. This includes connection time and reading the response.
NOTE: This is currently only implemented for creation and deletion of pods,
and creation of PVCs.
""",
)
k8s_api_request_retry_timeout = Integer(
30,
config=True,
help="""
Total timeout, including retry timeout, for kubernetes API calls
When a k8s API request connection times out, we retry it while backing
off exponentially. This lets you configure the total amount of time
we will spend trying an API request - including retries - before
giving up.
""",
)
events_enabled = Bool(
True,
config=True,
help="""
Enable event-watching for progress-reports to the user spawn page.
Disable if these events are not desirable
or to save some performance cost.
""",
)
enable_user_namespaces = Bool(
False,
config=True,
help="""
Cause each user to be spawned into an individual namespace.
This comes with some caveats. The Hub must run with significantly
more privilege (must have ClusterRoles analogous to its usual Roles)
and can therefore do heinous things to the entire cluster.
It will also make the Reflectors aware of pods and events across
all namespaces. This will have performance implications, although
using labels to restrict resource selection helps somewhat.
If you use this, consider cleaning up the user namespace in your
post_stop_hook.
""",
)
user_namespace_template = Unicode(
"{hubnamespace}-{username}",
config=True,
help="""
Template to use to form the namespace of user's pods (only if
enable_user_namespaces is True).
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
""",
)
namespace = Unicode(
config=True,
help="""
Kubernetes namespace to spawn user pods in.
Assuming that you are not running with enable_user_namespaces
turned on, if running inside a kubernetes cluster with service
accounts enabled, defaults to the current namespace, and if not,
defaults to `default`.
If you are running with enable_user_namespaces, this parameter
is ignored in favor of the `user_namespace_template` template
resolved with the hub namespace and the user name, with the
caveat that if the hub namespace is `default` the user
namespace will have the prefix `user` rather than `default`.
""",
)
@default('namespace')
def _namespace_default(self):
"""
Set namespace default to current namespace if running in a k8s cluster
If not in a k8s cluster with service accounts enabled, default to
`default`
"""
ns_path = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'
if os.path.exists(ns_path):
with open(ns_path) as f:
return f.read().strip()
return 'default'
ip = Unicode(
'0.0.0.0',
config=True,
help="""
The IP address (or hostname) the single-user server should listen on.
We override this from the parent so we can set a more sane default for
the Kubernetes setup.
""",
)
cmd = Command(
None,
allow_none=True,
minlen=0,
config=True,
help="""
The command used to start the single-user server.
Either
- a string containing a single command or path to a startup script
- a list of the command and arguments
- `None` (default) to use the Docker image's `CMD`
If `cmd` is set, it will be augmented with `spawner.get_args(). This will override the `CMD` specified in the Docker image.
""",
)
# FIXME: Don't override 'default_value' ("") or 'allow_none' (False) (Breaking change)
working_dir = Unicode(
None,
allow_none=True,
config=True,
help="""
The working directory where the Notebook server will be started inside the container.
Defaults to `None` so the working directory will be the one defined in the Dockerfile.
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
""",
)
# FIXME: Don't override 'default_value' ("") or 'allow_none' (False) (Breaking change)
service_account = Unicode(
None,
allow_none=True,
config=True,
help="""
The service account to be mounted in the spawned user pod.
The token of the service account is NOT mounted by default.
This makes sure that we don't accidentally give access to the whole
kubernetes API to the users in the spawned pods.
Set automount_service_account_token True to mount it.
This `serviceaccount` must already exist in the namespace the user pod is being spawned in.
""",
)
automount_service_account_token = Bool(
None,
allow_none=True,
config=True,
help="""
Whether to mount the service account token in the spawned user pod.
The default value is None, which mounts the token if the service account is explicitly set,
but doesn't mount it if not.
WARNING: Be careful with this configuration! Make sure the service account being mounted
has the minimal permissions needed, and nothing more. When misconfigured, this can easily
give arbitrary users root over your entire cluster.
""",
)
dns_name_template = Unicode(
"{name}.{namespace}.svc.cluster.local",
config=True,
help="""
Template to use to form the dns name for the pod.
""",
)
pod_name_template = Unicode(
'jupyter-{username}--{servername}',
config=True,
help="""
Template to use to form the name of user's pods.
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
Trailing `-` characters are stripped for safe handling of empty server names (user default servers).
This must be unique within the namespace the pods are being spawned
in, so if you are running multiple jupyterhubs spawning in the
same namespace, consider setting this to be something more unique.
.. versionchanged:: 0.12
`--` delimiter added to the template,
where it was implicitly added to the `servername` field before.
Additionally, `username--servername` delimiter was `-` instead of `--`,
allowing collisions in certain circumstances.
""",
)
pod_connect_ip = Unicode(
config=True,
help="""
The IP address (or hostname) of user's pods which KubeSpawner connects to.
If you do not specify the value, KubeSpawner will use the pod IP.
e.g. 'jupyter-{username}--{servername}.notebooks.jupyterhub.svc.cluster.local',
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
Trailing `-` characters in each domain level are stripped for safe handling of empty server names (user default servers).
This must be unique within the namespace the pods are being spawned
in, so if you are running multiple jupyterhubs spawning in the
same namespace, consider setting this to be something more unique.
""",
)
storage_pvc_ensure = Bool(
False,
config=True,
help="""
Ensure that a PVC exists for each user before spawning.
Set to true to create a PVC named with `pvc_name_template` if it does
not exist for the user when their pod is spawning.
""",
)
delete_pvc = Bool(
True,
config=True,
help="""Delete PVCs when deleting Spawners.
When a Spawner is deleted (not just stopped),
delete its associated PVC.
This occurs when a named server is deleted,
or when the user itself is deleted for the default Spawner.
Requires JupyterHub 1.4.1 for Spawner.delete_forever support.
.. versionadded: 0.17
""",
)
pvc_name_template = Unicode(
'claim-{username}--{servername}',
config=True,
help="""
Template to use to form the name of user's pvc.
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
Trailing `-` characters are stripped for safe handling of empty server names (user default servers).
This must be unique within the namespace the pvc are being spawned
in, so if you are running multiple jupyterhubs spawning in the
same namespace, consider setting this to be something more unique.
.. versionchanged:: 0.12
`--` delimiter added to the template,
where it was implicitly added to the `servername` field before.
Additionally, `username--servername` delimiter was `-` instead of `--`,
allowing collisions in certain circumstances.
""",
)
component_label = Unicode(
'singleuser-server',
config=True,
help="""
The component label used to tag the user pods. This can be used to override
the spawner behavior when dealing with multiple hub instances in the same
namespace. Usually helpful for CI workflows.
""",
)
secret_name_template = Unicode(
'jupyter-{username}{servername}',
config=True,
help="""
Template to use to form the name of user's secret.
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
This must be unique within the namespace the pvc are being spawned
in, so if you are running multiple jupyterhubs spawning in the
same namespace, consider setting this to be something more unique.
""",
)
secret_mount_path = Unicode(
"/etc/jupyterhub/ssl/",
allow_none=False,
config=True,
help="""
Location to mount the spawned pod's certificates needed for internal_ssl functionality.
""",
)
# FIXME: Don't override 'default_value' ("") or 'allow_none' (False) (Breaking change)
hub_connect_ip = Unicode(
allow_none=True,
config=True,
help="""DEPRECATED. Use c.JupyterHub.hub_connect_ip""",
)
hub_connect_port = Integer(
config=True, help="""DEPRECATED. Use c.JupyterHub.hub_connect_url"""
)
@observe('hub_connect_ip', 'hub_connect_port')
def _deprecated_changed(self, change):
warnings.warn(
"""
KubeSpawner.{0} is deprecated with JupyterHub >= 0.8.
Use JupyterHub.{0}
""".format(
change.name
),
DeprecationWarning,
)
setattr(self.hub, change.name.split('_', 1)[1], change.new)
common_labels = Dict(
{
'app': 'jupyterhub',
'heritage': 'jupyterhub',
},
config=True,
help="""
Kubernetes labels that both spawned singleuser server pods and created
user PVCs will get.
Note that these are only set when the Pods and PVCs are created, not
later when this setting is updated.
""",
)
extra_labels = Dict(
config=True,
help="""
Extra kubernetes labels to set on the spawned single-user pods, as well
as on the pods' associated k8s Service and k8s Secret if internal_ssl is
enabled.
The keys and values specified here would be set as labels on the spawned single-user
kubernetes pods. The keys and values must both be strings that match the kubernetes
label key / value constraints.
See `the Kubernetes documentation <https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/>`__
for more info on what labels are and why you might want to use them!
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
""",
)
extra_annotations = Dict(
config=True,
help="""
Extra Kubernetes annotations to set on the spawned single-user pods, as
well as on the pods' associated k8s Service and k8s Secret if
internal_ssl is enabled.
The keys and values specified here are added as annotations on the spawned single-user
kubernetes pods. The keys and values must both be strings.
See `the Kubernetes documentation <https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/>`__
for more info on what annotations are and why you might want to use them!
`{username}`, `{userid}`, `{servername}`, `{hubnamespace}`,
`{unescaped_username}`, and `{unescaped_servername}` will be expanded if
found within strings of this configuration. The username and servername
come escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
""",
)
image = Unicode(
'jupyterhub/singleuser:latest',
config=True,
help="""
Docker image to use for spawning user's containers.
Defaults to `jupyterhub/singleuser:latest`
Name of the container + a tag, same as would be used with
a `docker pull` command. If tag is set to `latest`, kubernetes will
check the registry each time a new user is spawned to see if there
is a newer image available. If available, new image will be pulled.
Note that this could cause long delays when spawning, especially
if the image is large. If you do not specify a tag, whatever version
of the image is first pulled on the node will be used, thus possibly
leading to inconsistent images on different nodes. For all these
reasons, it is recommended to specify a specific immutable tag
for the image.
If your image is very large, you might need to increase the timeout
for starting the single user container from the default. You can
set this with::
c.KubeSpawner.start_timeout = 60 * 5 # Up to 5 minutes
""",
)
image_pull_policy = Unicode(
'IfNotPresent',
config=True,
help="""
The image pull policy of the docker container specified in
`image`.
Defaults to `IfNotPresent` which causes the Kubelet to NOT pull the image
specified in KubeSpawner.image if it already exists, except if the tag
is `:latest`. For more information on image pull policy,
refer to `the Kubernetes documentation <https://kubernetes.io/docs/concepts/containers/images/>`__.
This configuration is primarily used in development if you are
actively changing the `image_spec` and would like to pull the image
whenever a user container is spawned.
""",
)
image_pull_secrets = Union(
trait_types=[
List(),
Unicode(),
],
config=True,
help="""
A list of references to Kubernetes Secret resources with credentials to
pull images from image registries. This list can either have strings in
it or objects with the string value nested under a name field.
Passing a single string is still supported, but deprecated as of
KubeSpawner 0.14.0.
See `the Kubernetes documentation
<https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod>`__
for more information on when and why this might need to be set, and what
it should be set to.
""",
)
@validate('image_pull_secrets')
def _validate_image_pull_secrets(self, proposal):
if type(proposal['value']) == str:
warnings.warn(
"""Passing KubeSpawner.image_pull_secrets string values is
deprecated since KubeSpawner 0.14.0. The recommended
configuration is now a list of either strings or dictionary
objects with the string referencing the Kubernetes Secret name
in under the value of the dictionary's name key.""",
DeprecationWarning,
)
return [{"name": proposal['value']}]
return proposal['value']
node_selector = Dict(
config=True,
help="""
The dictionary Selector labels used to match the Nodes where Pods will be launched.
Default is None and means it will be launched in any available Node.
For example to match the Nodes that have a label of `disktype: ssd` use::
c.KubeSpawner.node_selector = {'disktype': 'ssd'}
""",
)
uid = Union(
trait_types=[
Integer(),
Callable(),
],
default_value=None,
allow_none=True,
config=True,
help="""
The UID to run the single-user server containers as.
This UID should ideally map to a user that already exists in the container
image being used. Running as root is discouraged.
Instead of an integer, this could also be a callable that takes as one
parameter the current spawner instance and returns an integer. The callable
will be called asynchronously if it returns a future. Note that
the interface of the spawner class is not deemed stable across versions,
so using this functionality might cause your JupyterHub or kubespawner
upgrades to break.
If set to `None`, the user specified with the `USER` directive in the
container metadata is used.
""",
)
gid = Union(
trait_types=[
Integer(),
Callable(),
],
default_value=None,
allow_none=True,
config=True,
help="""
The GID to run the single-user server containers as.
This GID should ideally map to a group that already exists in the container
image being used. Running as root is discouraged.
Instead of an integer, this could also be a callable that takes as one
parameter the current spawner instance and returns an integer. The callable
will be called asynchronously if it returns a future. Note that
the interface of the spawner class is not deemed stable across versions,
so using this functionality might cause your JupyterHub or kubespawner
upgrades to break.
If set to `None`, the group of the user specified with the `USER` directive
in the container metadata is used.
""",
)
fs_gid = Union(
trait_types=[
Integer(),
Callable(),
],
default_value=None,
allow_none=True,
config=True,
help="""
The GID of the group that should own any volumes that are created & mounted.
A special supplemental group that applies primarily to the volumes mounted
in the single-user server. In volumes from supported providers, the following
things happen:
1. The owning GID will be the this GID
2. The setgid bit is set (new files created in the volume will be owned by
this GID)
3. The permission bits are OR’d with rw-rw
The single-user server will also be run with this gid as part of its supplemental
groups.
Instead of an integer, this could also be a callable that takes as one
parameter the current spawner instance and returns an integer. The callable will
be called asynchronously if it returns a future, rather than an int. Note that
the interface of the spawner class is not deemed stable across versions,
so using this functionality might cause your JupyterHub or kubespawner
upgrades to break.
You'll *have* to set this if you are using auto-provisioned volumes with most
cloud providers. See `fsGroup <https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#podsecuritycontext-v1-core>`__
for more details.
""",
)
supplemental_gids = Union(
trait_types=[
List(),
Callable(),
],
config=True,
help="""
A list of GIDs that should be set as additional supplemental groups to the
user that the container runs as.
Instead of a list of integers, this could also be a callable that takes as one
parameter the current spawner instance and returns a list of integers. The
callable will be called asynchronously if it returns a future, rather than
a list. Note that the interface of the spawner class is not deemed stable
across versions, so using this functionality might cause your JupyterHub
or kubespawner upgrades to break.
You may have to set this if you are deploying to an environment with RBAC/SCC
enforced and pods run with a 'restricted' SCC which results in the image being
run as an assigned user ID. The supplemental group IDs would need to include
the corresponding group ID of the user ID the image normally would run as. The
image must setup all directories/files any application needs access to, as group
writable.
""",
)
privileged = Bool(
False,
config=True,
help="""
Whether to run the pod with a privileged security context.
""",
)
allow_privilege_escalation = Bool(
True,
config=True,
help="""
Controls whether a process can gain more privileges than its parent process.
This bool directly controls whether the no_new_privs flag gets set on the container
process.
AllowPrivilegeEscalation is true always when the container is:
1) run as Privileged OR 2) has CAP_SYS_ADMIN.
""",
)
container_security_context = Union(
trait_types=[
Dict(),
Callable(),
],
config=True,
help="""
A Kubernetes security context for the container. Note that all
configuration options within here should be camelCased.
What is configured here has the highest priority, so the alternative
configuration `uid`, `gid`, `privileged`, and
`allow_privilege_escalation` will be overridden by this.
Rely on `the Kubernetes reference
<https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#securitycontext-v1-core>`__
for details on allowed configuration.
""",
)
pod_security_context = Union(
trait_types=[
Dict(),
Callable(),
],
config=True,
help="""
A Kubernetes security context for the pod. Note that all configuration
options within here should be camelCased.
What is configured here has higher priority than `fs_gid` and
`supplemental_gids`, but lower priority than what is set in the
`container_security_context`.
Note that anything configured on the Pod level will influence all
containers, including init containers and sidecar containers.
Rely on `the Kubernetes reference
<https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#podsecuritycontext-v1-core>`__
for details on allowed configuration.
""",
)
modify_pod_hook = Callable(
None,
allow_none=True,
config=True,
help="""
Callable to augment the Pod object before launching.
Expects a callable that takes two parameters:
1. The spawner object that is doing the spawning
2. The Pod object that is to be launched
You should modify the Pod object and return it.
This can be a coroutine if necessary. When set to none, no augmenting is done.
This is very useful if you want to modify the pod being launched dynamically.
Note that the spawner object can change between versions of KubeSpawner and JupyterHub,
so be careful relying on this!
""",
)
volumes = List(
config=True,
help="""
List of Kubernetes Volume specifications that will be mounted in the user pod.
This list will be directly added under `volumes` in the kubernetes pod spec,
so you should use the same structure. Each item in the list must have the