Comments (16)
@betatim @yuvipanda I cloned the doc under my personal google account so it's easier to share it publicly. New doc at https://docs.google.com/document/d/1oVqZnbs4u57DxBuTqc9TD40pOM1FSpBOFlV3BW5j8Fo/edit?usp=sharing.
from kubespawner.
I don't see a clear feature suggestion, and since the linked document isn't readable by me, I'll close this.
For users that wants to mount secrets individual to the users, I would suggest the mechanism of configuring c.KubeSpawner.pre_spawn_hook to a function that dynamically configures the user-individual spawner object based on properties of the user, for example then updating the user individual spawners volumes
and volume_mounts
to mount a k8s secret for that user-pod specifically.
from kubespawner.
Awesome, re: design doc! Thanks, @liyinan926!
from kubespawner.
This was the doc from @liyinan926 - https://docs.google.com/document/d/1DDm7N2sZ4wSHdCzzcsHrtp8rvOttGuMn7MadxScvfnI/edit?ts=5a00b644#heading=h.14lxv2es4gqw
I'm at a conference until wednesday unfortunately! I'll look through it as soon as I can!
from kubespawner.
@yuvipanda just wondering have you got a chance to look at the doc?
from kubespawner.
I can't read the document :( Would be good to adjust the permissions so it can be read by anyone without having to request permissions.
from kubespawner.
@liyinan926 I have read it and have Opinions(tm) :) I'll respond in more detail later today.
Can you open the doc up to public viewing / commenting?
from kubespawner.
@liyinan926 I didn't make it today, sorry! Shall tomorrow afternoon!
from kubespawner.
@liyinan926 thank you for your patience and your hard work in making the design doc! We highly appreciate your help!
I've been thinking about the configuration practices for JupyterHub in general and KubeSpawner in particular, and I think here are some general principles we should try hold to:
- All config should be in the form of traitlets only. This allows for easy testing & is consistent with the whole of the JupyterHub ecosystem. The current value of all the configurable traitlets of a spawner should fully capture its configuration, without requiring references to external sources (like configmaps)
- We should make common configuration cases easy
- We should make complex, advanced configuration cases possible by depending on the fact that
jupyterhub_config.py
file is Python code, and you as an admin can actually write code here. You can subclass things, pass in callables, read from the filesystem, etc.
These haven't really been written down anywhere else, so thank you for starting this conversation in such a design centric way!
I've tried to undersand what you had intended to convey with the design doc, and I think we can make a nice solution that sticks to the traitlets design instead of mixing traitlets + configmap. In this comment, I shall outline the current amount of dynamism available, and next steps to meet the requirements I understand from the doc. Let me know if I've misunderstood anything!
Current situation
You can currently mount secrets by modifying c.KubeSpawner.volumes
and c.KubeSpawner.volume_mounts
. However, this is not fully static - you can use '{username}' and '{userid}' in these traitlets(https://github.com/jupyterhub/kubespawner/blob/master/kubespawner/spawner.py#L608). This enforces naming conventions on admins, but means you can already do per-user secret mounts.
New features needed
I think to achieve the full amount of features that is present in the design doc, we can do a bunch of smaller, well scoped improvements to kubespawner.
More dynamic customization than just username / userid
Just username and userid might not be enough to figure out which secrets to mount. To begin with, we might want to augment the template expansion with more information:
- Data from options form that may be submitted pre-spawn
- Information from auth_state (this could include what 'class' of user they are, etc)
- ???
We could also allow callables here - although I'm somewhat loathe to do that without concrete use cases. Usually for callables we have to provide them with the spawner object, and so this means admins' config files might break badly with each version upgrade...
Environment variables from configmaps / secrets / downward API
Currently you can only specify environment variables with c.KubeSpawner.environment
, and they can either be static key value pairs or 'dynamic' where the value is determined by a callable. There's no way to plug in an environment variable sourced from a ConfigMap, Secret or Downward API. We can fix this by allowing values for c.KubeSpawner.environment
to be dicts themselves, thus allowing use of secrets / configmap / downward API.
Dynamic reloading of config without a restart
One of the benefits of putting config in configmap rather than as traitlets is dynamic configuration - as the configmap changes, your app's configuration also changes dyanmically without needing a restart. With a pure traitlets situation, you can not do this seamlessly right now.
I don't have a clean solution for this yet. One possibility is to make your jupyterhub_config.py read its config from a ConfigMap projected as a volume into the hub's pod (like https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/master/images/hub/jupyterhub_config.py#L6), and have a process supervisor restart the hub process whenever the contents of the configmap dir changes.
Out of scope
- This is all independent of creating / deleting new namespaces.
- It doesn't talk about namespace specific config vs user specific config. This comment assumes that what you really want is user-specific config.
Next steps?
I think the three individual items here can be implemented independently of each other simply. Perhaps that's the next step?
It's also possible I've misunderstood what the design doc was trying to accomplish, and have hence produced alternatives against a strawman. I'm happy to jump on a video call (or make a trip to the South Bay!) to clarify if that helps.
Again, thank you very much for your work, @foxish and @liyinan926! We appreciate it :) My backlog has cleared up a bit + I'm not travelling this week, so my responses should be quicker from now.
from kubespawner.
Also /cc @minrk who is the lead dev on JupyterHub and might have opinions too.
from kubespawner.
@yuvipanda Thanks for the awesome write up! Looking forward to the new features!
from kubespawner.
@liyinan926 do you want to pick up work on any of them? :)
from kubespawner.
I really like the idea of using a Callable
for traitlets in general, since it gives control to the user over how configuration is loaded - and it can be dynamic without having to trigger restarts.
However, I understand the hesitation in passing the KubeSpawner
instance as an argument, given that the API isn't necessarily fixed.
BUT: Most of these dynamic configurations are asking for per-user configuration values. The only API surface needed would be the user
field; even passing that in directly (and not passing the spawner reference at all) should be sufficient for any of these. I know that access to the ID would be almost enough; and access to the ID + the decoded auth data (user groups) would be 100% sufficient.
Thoughts?
from kubespawner.
Hi Folks,
Its possible to provide examples about configmap mounting for the singleuser image? I have the following code that use s3-config for singleuser image when the env variable 'USE_S3' is defined for the hub config. The config is the following:
if use_s3_storage:
c.KubeSpawner.env_keep += [
"S3_ENDPOINT_URL",
"S3_ACCESS_KEY",
"S3_SECRET_KEY",
"S3_BUCKET",
"S3_CONTENT_PREFIX"
]
#Reference S3 Configuration for use with the notebook.
c.KubeSpawner.volumes = [
{'name': 'notebook-config',
'configMap': {'name': 'jupyter-user-s3-v1',
'items': {'key': 'jupyter_notebook_s3_config.py',
'path': 'jupyter_notebook_s3_config.py'
}
}
}
]
#Define s3config as jupyter notebook config.
c.KubeSpawner.volume_mounts = [
{'name': 'jupyter-user-s3-v1',
'mountPath': '/home/jovyan/.jupyter/jupyter_notebook_config.py'
},
]
I tried with this code, but it doesnt work ... So I turn on the debug statements and the following JSON was generated by the spawner when it tried to submit the request (which results in BadRequestErrror):
[E 2018-09-03 21:38:00.198 JupyterHub spawner:979] Failed for {'api_version': 'v1',
'kind': 'Pod',
'metadata': {'annotations': {'hub.jupyter.org/username': '01388863189'},
'cluster_name': None,
'creation_timestamp': None,
'deletion_grace_period_seconds': None,
'deletion_timestamp': None,
'finalizers': None,
'generate_name': None,
'generation': None,
'initializers': None,
'labels': {'app': 'jupyterhub',
'component': 'singleuser-server',
'heritage': 'jupyterhub'},
'name': 'jupyter-01388863189',
'namespace': None,
'owner_references': None,
'resource_version': None,
'self_link': None,
'uid': None},
'spec': {'active_deadline_seconds': None,
'affinity': None,
'automount_service_account_token': False,
'containers': [{'args': None,
'command': None,
'env': [{'name': 'S3_ENDPOINT_URL',
'value': 'http://minio-0.default.svc.cluster.local:9000',
'value_from': None},
{'name': 'S3_ACCESS_KEY',
'value': 'ORCUGXYV5GHE5HOGW5N5',
'value_from': None},
{'name': 'S3_SECRET_KEY',
'value': 'XkTz2EJ068LxtnfK99E95nxgNx3DRvazZfsofnai',
'value_from': None},
{'name': 'S3_BUCKET',
'value': 'esint',
'value_from': None},
{'name': 'S3_CONTENT_PREFIX',
'value': 'notebooks/esint',
'value_from': None},
{'name': 'JUPYTERHUB_API_TOKEN',
'value': '9207e85361764de286eafa8fee28115d',
'value_from': None},
{'name': 'JPY_API_TOKEN',
'value': '9207e85361764de286eafa8fee28115d',
'value_from': None},
{'name': 'JUPYTERHUB_CLIENT_ID',
'value': 'jupyterhub-user-01388863189',
'value_from': None},
{'name': 'JUPYTERHUB_HOST',
'value': '',
'value_from': None},
{'name': 'JUPYTERHUB_OAUTH_CALLBACK_URL',
'value': '/user/01388863189/oauth_callback',
'value_from': None},
{'name': 'JUPYTERHUB_USER',
'value': '01388863189',
'value_from': None},
{'name': 'JUPYTERHUB_API_URL',
'value': 'http://10.233.14.85:8081/hub/api',
'value_from': None},
{'name': 'JUPYTERHUB_BASE_URL',
'value': '/',
'value_from': None},
{'name': 'JUPYTERHUB_SERVICE_PREFIX',
'value': '/user/01388863189/',
'value_from': None}],
'env_from': None,
'image': 'hub.hom.estaleiro.serpro/ia/jupyterhub-singleuser:0.9-1',
'image_pull_policy': 'IfNotPresent',
'lifecycle': {},
'liveness_probe': None,
'name': 'notebook',
'ports': [{'container_port': 8888,
'host_ip': None,
'host_port': None,
'name': 'notebook-port',
'protocol': None}],
'readiness_probe': None,
'resources': {'limits': {}, 'requests': {}},
'security_context': None,
'stdin': None,
'stdin_once': None,
'termination_message_path': None,
'termination_message_policy': None,
'tty': None,
'volume_mounts': [{'mountPath': '/home/jovyan/.jupyter/jupyter_notebook_config.py',
'name': 'jupyter-user-s3-v1'},
{'mount_path': '/var/run/secrets/kubernetes.io/serviceaccount',
'name': 'no-api-access-please',
'read_only': True,
'sub_path': None}],
'working_dir': None}],
'dns_policy': None,
'host_aliases': None,
'host_ipc': None,
'host_network': None,
'host_pid': None,
'hostname': None,
'image_pull_secrets': None,
'init_containers': [],
'node_name': None,
'node_selector': None,
'restart_policy': None,
'scheduler_name': None,
'security_context': {'fs_group': 0,
'run_as_non_root': None,
'run_as_user': 0,
'se_linux_options': None,
'supplemental_groups': None},
'service_account': None,
'service_account_name': None,
'subdomain': None,
'termination_grace_period_seconds': None,
'tolerations': None,
'volumes': [{'configMap': {'items': {'key': 'jupyter_notebook_s3_config.py',
'path': 'jupyter_notebook_s3_config.py'},
'name': 'jupyter-user-s3-v1'},
'name': 'notebook-config'},
{'aws_elastic_block_store': None,
'azure_disk': None,
'azure_file': None,
'cephfs': None,
'cinder': None,
'config_map': None,
'downward_api': None,
'empty_dir': {},
'fc': None,
'flex_volume': None,
'flocker': None,
'gce_persistent_disk': None,
'git_repo': None,
'glusterfs': None,
'host_path': None,
'iscsi': None,
'name': 'no-api-access-please',
'nfs': None,
'persistent_volume_claim': None,
'photon_persistent_disk': None,
'portworx_volume': None,
'projected': None,
'quobyte': None,
'rbd': None,
'scale_io': None,
'secret': None,
'storageos': None,
'vsphere_volume': None}]},
'status': None}
Traceback (most recent call last):
File "/opt/conda/lib/python3.6/site-packages/kubespawner/spawner.py", line 973, in start
pod
File "/opt/conda/lib/python3.6/concurrent/futures/thread.py", line 56, in run
result = self.fn(*self.args, **self.kwargs)
File "/opt/conda/lib/python3.6/site-packages/kubespawner/spawner.py", line 942, in asynchronize
return method(*args, **kwargs)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/apis/core_v1_api.py", line 6561, in create_namespaced_pod
(data) = self.create_namespaced_pod_with_http_info(namespace, body, **kwargs)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/apis/core_v1_api.py", line 6651, in create_namespaced_pod_with_http_info
collection_formats=collection_formats)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 335, in call_api
_preload_content, _request_timeout)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 148, in __call_api
_request_timeout=_request_timeout)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 393, in request
body=body)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/rest.py", line 287, in POST
body=body)
File "/opt/conda/lib/python3.6/site-packages/kubernetes/client/rest.py", line 240, in request
raise ApiException(http_resp=r)
kubernetes.client.rest.ApiException: (400)
Reason: Bad Request
HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json', 'Date': 'Mon, 03 Sep 2018 21:37:26 GMT', 'Content-Length': '538'})
HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Pod in version \"v1\" cannot be handled as a Pod: v1.Pod: Spec: v1.PodSpec: Volumes: []v1.Volume: v1.Volume: VolumeSource: ConfigMap: v1.ConfigMapVolumeSource: Items: []v1.KeyToPath: ReadArrayCB: expect [ or n, but found {, error found in #10 byte of ...|\"items\": {\"key\": \"ju|..., bigger context ...|figMap\": {\"name\": \"jupyter-user-s3-v1\", \"items\": {\"key\": \"jupyter_notebook_s3_config.py\", \"path\": \"j|...","reason":"BadRequest","code":400}
So ... Do you guys have an appointment or an example on how to use configMap to help me with this?
Thanks in advance.
from kubespawner.
I tried to change the code to
c.KubeSpawner.volumes = [
{'name': 'notebook-config',
'configMap': {'name': 'jupyter-user-s3-v1',
'items': [{'key': 'jupyter_notebook_s3_config.py',
'path': 'jupyter_notebook_s3_config.py'
}]
}
}
]
Looks like items should be a list (of course). Corrected with the above code. After that, a small correction in the name of the volume made my day.
c.KubeSpawner.volume_mounts = [
{'name': 'notebook-config',
'mountPath': '/home/jovyan/.jupyter/jupyter_notebook_config.py'
},
]
Anyway, a set of config examples would help alot. Is it a valid concern for the project? I can make some PRs with examples if it is.
from kubespawner.
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there:
https://discourse.jupyter.org/t/user-specific-aws-secrets/6391/1
from kubespawner.
Related Issues (20)
- Servers being reported as down after hub restart that are not HOT 1
- KubeSpawner `latest` docs is missing some traitlets help HOT 4
- Doc blocks contain dead links to kubernetes API v1.20 HOT 3
- Kill k8s pods earlier when specific errors appear HOT 4
- Object has no attribute 'cancel' - a secondary error when handling a timeout error HOT 5
- certificate verify failed: self signed certificate HOT 4
- Options independent of any profile HOT 4
- Kubespawner fails to create service when internal_ssl is activated because of the metadata HOT 1
- KubeSpawner does not respect uid for mounts HOT 1
- Support for callables in env no longer works HOT 6
- Test failures in main branch HOT 1
- Broken Links to Kubernetes-docs in main README HOT 1
- c.KubeSpawner.environment not interpreting callables HOT 2
- JupyterHub Singleuser Port Routing Issue After Restart HOT 3
- Standalone Pods found when Hub pod restart HOT 3
- Cannot build wheel for package due to folder name being different to package name HOT 3
- Are we testing against JupyterHub 1.3?
- Stop hardcoding reflectors to monitor `component: singleuser-server` label
- Create new labels that we can switch over to later HOT 7
- Bring into line with the Kubernetes operator design pattern HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from kubespawner.