diff --git a/docs/source/index.md b/docs/source/index.md index 92726951..d13bc854 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -13,7 +13,7 @@ management of containerized applications. If you want to run a JupyterHub setup that needs to scale across multiple nodes (anything with over ~50 simultaneous users), Kubernetes is a wonderful way to do it. Features include: -- Easily and elasticly run anywhere between 2 and thousands of nodes with the +- Easily and elastically run anywhere between 2 and thousands of nodes with the same set of powerful abstractions. Scale up and down as required by simply adding or removing nodes. @@ -81,5 +81,6 @@ utils ```{toctree} :maxdepth: 2 :caption: Reference +templates changelog ``` diff --git a/docs/source/ssl.md b/docs/source/ssl.md index 9af8bcfb..a6677f96 100644 --- a/docs/source/ssl.md +++ b/docs/source/ssl.md @@ -8,7 +8,7 @@ If enabled, the Kubespawner will mount the internal_ssl certificates as Kubernet To enable, use the following settings: -``` +```python c.JupyterHub.internal_ssl = True c.JupyterHub.spawner_class = 'kubespawner.KubeSpawner' @@ -16,8 +16,8 @@ c.JupyterHub.spawner_class = 'kubespawner.KubeSpawner' Further configuration can be specified with the following (listed with their default values): -``` -c.KubeSpawner.secret_name_template = "jupyter-{username}{servername}" +```python +c.KubeSpawner.secret_name_template = "{pod_name}" c.KubeSpawner.secret_mount_path = "/etc/jupyterhub/ssl/" ``` diff --git a/docs/source/templates.md b/docs/source/templates.md new file mode 100644 index 00000000..6b17da17 --- /dev/null +++ b/docs/source/templates.md @@ -0,0 +1,156 @@ +(templates)= + +# Templated fields + +Several fields in KubeSpawner can be resolved as string templates, +so each user server can get distinct values from the same configuration. + +String templates use the Python formatting convention of `f"{fieldname}"`, +so for example the default `pod_name_template` of `"jupyter-{user_server}"` will produce: + +| username | server name | pod name | +| ---------------- | ----------- | ---------------------------------------------- | +| `user` | `''` | `jupyter-user` | +| `user` | `server` | `jupyter-user--server` | +| `user@email.com` | `Some Name` | `jupyter-user-email-com--some-name---0c1fe94b` | + +## templated properties + +Some common templated fields: + +- [pod_name_template](#KubeSpawner.pod_name_template) +- [pvc_name_template](#KubeSpawner.pvc_name_template) +- [working_dir](#KubeSpawner.working_dir) + +## fields + +The following fields are available in templates: + +| field | description | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `{username}` | the username passed through the configured slug scheme | +| `{servername}` | the name of the server passed through the configured slug scheme (`''` for the user's default server) | +| `{user_server}` | the username and servername together as a single slug. This should be used most places for a unique string for a given user's server (new in kubespawner 7). | +| `{unescaped_username}` | the actual username without escaping (no guarantees about value, except as enforced by your Authenticator) | +| `{unescaped_servername}` | the actual server name without escaping (no guarantees about value) | +| `{pod_name}` | the resolved pod name, often a good choice if you need a starting point for other resources (new in kubespawner 7) | +| `{pvc_name}` | the resolved PVC name (new in kubespawner 7) | +| `{namespace}` | the kubernetes namespace of the server (new in kubespawner 7) | +| `{hubnamespace}` | the kubernetes namespace of the Hub | + +Because there are two escaping schemes for `username`, `servername`, and `user_server`, you can explicitly select one or the other on a per-template-field basis with the prefix `safe_` or `escaped_`: + +| field | description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `{escaped_username}` | the username passed through the old 'escape' slug scheme | +| `{escaped_servername}` | the server name passed through the 'escape' slug scheme | +| `{escaped_user_server}` | the username and servername together as a single slug, identical to `"{escaped_username}--{escaped_servername}".rstrip("-")` (new in kubespawner 7) | +| `{safe_username}` | the username passed through the 'safe' slug scheme (new in kubespawner 7) | +| `{safe_servername}` | the server name passed through the 'safe' slug scheme (new in kubespawner 7) | +| `{safe_user_server}` | the username and server name together as a 'safe' slug (new in kubespawner 7) | + +These may be useful during a transition upgrading a deployment from an earlier version of kubespawner. + +The value of the unprefixed `username`, etc. is goverend by the [](#KubeSpawner.slug_scheme) configuration, and always matches exactly one of these values. + +## Template tips + +In general, these guidelines should help you pick fields to use in your template strings: + +- use `{user_server}` when a string should be unique _per server_ (e.g. pod name) +- use `{username}` when it should be unique per user, but shared across named servers (sometimes chosen for PVCs) +- use `{escaped_}` prefix if you need to keep certain values unchanged in a deployment upgrading from kubespawner \< 7 +- `{pod_name}` can be re-used anywhere you want to create more resources associated with a given pod, + to avoid repeating yourself + +## Changing template configuration + +Changing configuration should not generally affect _running_ servers. +However, when changing a property that may need to persist across user server restarts, special consideration may be required. +For example, changing `pvc_name` or `working_dir` could result in disconnecting a user's server from data loaded in previous sessions. +This may be your intention or not! KubeSpawner cannot know. + +`pvc_name` is handled specially, to avoid losing access to data. +If `KubeSpawner.remember_pvc_name` is True, once a server has started, a server's PVC name cannot be changed by configuration. +Any future launch will use the previous `pvc_name`, regardless of change in configuration. +If you _want_ to change the names of mounted PVCs, you can set + +```python +c.KubeSpawner.remember_pvc_name = False +``` + +This handling isn't general for PVCs, only specifically the default `pvc_name`. +If you have defined your own volumes, you need to handle changes to these yourself. + +## Upgrading from kubespawner \< 7 + +Prior to kubespawner 7, an escaping scheme was used that ensured values were _unique_, +but did not always ensure fields were _valid_. +In particular: + +- start/end rules were not enforced +- length was not enforced + +This meant that e.g. usernames that start with a capital letter or were very long could result in servers failing to start because the escaping scheme produced an invalid label. +To solve this, a new 'safe' scheme has been added in kubespawner 7 for computing template strings, +which aims to guarantee to always produce valid object names and labels. +The new scheme is the default in kubespawner 7. + +You can select the scheme with: + +```python +c.KubeSpawner.slug_scheme = "escape" # no changes from kubespawner 6 +c.KubeSpawner.slug_scheme = "safe" # default for kubespawner 7 +``` + +The new scheme has the following rules: + +- the length of any _single_ template field is limited to 48 characters (the total length of the string is not enforced) +- the result will only contain lowercase ascii letters, numbers, and `-` +- it will always start and end with a letter or number +- if a name is 'safe', it is used unmodified +- if any escaping is required, a truncated safe subset of characters is used, followed by `---{hash}` where `{hash}` is a checksum of the original input string +- `-` shall not occur in sequences of more than one consecutive `-`, except where inserted by the escaping mechanism +- if no safe characters are present, 'x' is used for the 'safe' subset + +Since length requirements are applied on a per-field basis, a new `{user_server}` field is added, +which computes a single valid slug following the above rules which is unique for a given user server. +The general form is: + +``` +{username}--{servername}---{hash} +``` + +where + +- `--{servername}` is only present for non-empty server names +- `---{hash}` is only present if escaping is required for _either_ username or servername, and hashes the combination of user and server. + +This `{user_server}` is the recommended value to use in pod names, etc. +In the escape scheme, `{user_server}` is identical to the previous value used in default templates: `{username}--{servername}`, +so it should be safe to upgrade previous templated using `{username}--{servername}` to `{user_server}` or `{escaped_user_server}`. + +In the vast majority of cases (where no escaping is required), the 'safe' scheme produces identical results to the 'escape' scheme. +Probably the most common case where the two differ is in the presence of single `-` characters, which the `escape` scheme escaped to `-2d`, while the 'safe' scheme does not. + +Examples: + +| name | escape scheme | safe scheme | +| username | username | username | +| has-hyphen | has-2dhyphen | has-hyphen | +| Capital | `-43apital` (error) | `capital---1a1cf792` | +| user@email.com | 'user-40email-2ecom' | 'user-email-com---0925f997' | +| 'a-very-long-name-that-is-too-long-for-sixty-four-character-labels' | 'a-2dvery-2dlong-2dname-2dthat-2dis-2dtoo-2dlong-2dfor-2dsixty-2dfour-2dcharacter-2dlabels' (error) | 'a-very-long-name-that-is-too-long-for---29ac5fd2' | +| ALLCAPS | '-41-4c-4c-43-41-50-53' (error) | 'allcaps---27c6794c'| + +Most changed names won't have a practical effect. +However, to avoid `pvc_name` changing even though KubeSpawner 6 didn't persist it, +on first launch (for each server) after upgrade KubeSpawner checks if: + +1. `pvc_name_template` produces a different result with `scheme='escape'` +1. a pvc with the old 'escaped' name exists + +and if such a pvc exists, the older name is used instead of the new one (it is then remembered for subsequent launches, according to `remember_pvc_name`). +This is an attempt to respect the `remember_pvc_name` configuration, even though the old name is not technically recorded. +We can infer the old value, as long as configuration has not changed. +This will only work if upgrading KubeSpawer does not _also_ coincide with a change in the `pvc_name_template` configuration. diff --git a/kubespawner/spawner.py b/kubespawner/spawner.py index d73388df..f30cc670 100644 --- a/kubespawner/spawner.py +++ b/kubespawner/spawner.py @@ -341,20 +341,13 @@ def __init__(self, *args, **kwargs): 'escape' scheme: - does not guarantee correct names, e.g. does not handle capital letters or length - - escapes fields one at a time in templates: - - `{username}` is the _escaped_ username - - `{servername}` is the _escaped_ server name 'safe' scheme: - should guarantee correct names - - escapes the whole string at once - escapes only if needed - - in templates: - - `{username}` is the user's actual name - - `{escaped_username}` is the escaped username, used in the previous scheme - - `{servername}` is the user's actual name - - `{escaped_servername}` is the escaped server name, used in the previous scheme + - enforces length requirements + - uses hash to avoid collisions when escaping is required 'safe' is the default and preferred as it produces both: @@ -376,11 +369,10 @@ def __init__(self, *args, **kwargs): Note that these are only set when the namespaces are created, not later when this setting is updated. - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -393,11 +385,10 @@ def __init__(self, *args, **kwargs): Note that these are only set when the namespaces are created, not later when this setting is updated. - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -408,11 +399,10 @@ def __init__(self, *args, **kwargs): 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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -496,11 +486,10 @@ def _namespace_default(self): 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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -551,18 +540,14 @@ def _namespace_default(self): 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 - `__. - - 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. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + .. versionchanged:: 0.12 `--` delimiter added to the template, where it was implicitly added to the `servername` field before. @@ -579,11 +564,9 @@ def _namespace_default(self): e.g. `{pod_name}.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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. Trailing `-` characters in each domain level are stripped for safe handling of empty server names (user default servers). @@ -626,11 +609,6 @@ def _namespace_default(self): 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 - `__. Trailing `-` characters are stripped for safe handling of empty server names (user default servers). @@ -638,6 +616,10 @@ def _namespace_default(self): in, so if you are running multiple jupyterhubs spawning in the same namespace, consider setting this to be something more unique. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + .. versionchanged:: 0.12 `--` delimiter added to the template, where it was implicitly added to the `servername` field before. @@ -674,20 +656,12 @@ def _namespace_default(self): ) secret_name_template = Unicode( - 'jupyter-{username}{servername}', + '{pod_name}', 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 - `__. - - 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. + Default: same as `pod_name`. It is unlikely that this should be changed. """, ) @@ -757,11 +731,9 @@ def _deprecated_changed(self, change): See `the Kubernetes documentation `__ 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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. """, ) @@ -778,9 +750,10 @@ def _deprecated_changed(self, change): See `the Kubernetes documentation `__ 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. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1123,11 +1096,10 @@ def _validate_image_pull_secrets(self, proposal): for more information on the various kinds of volumes available and their options. Your kubernetes cluster must already be configured to support the volume types you want to use. - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1146,11 +1118,10 @@ def _validate_image_pull_secrets(self, proposal): See `the Kubernetes documentation `__ for more information on how the `volumeMount` item works. - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1190,9 +1161,10 @@ def _validate_image_pull_secrets(self, proposal): See `the Kubernetes documentation `__ 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. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1208,11 +1180,10 @@ def _validate_image_pull_secrets(self, proposal): See `the Kubernetes documentation `__ 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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1270,11 +1241,10 @@ def _validate_image_pull_secrets(self, proposal): c.KubeSpawner.storage_selector = {'matchLabels':{'content': 'jupyter'}} - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -1406,11 +1376,10 @@ def _validate_image_pull_secrets(self, proposal): "command": ["/usr/local/bin/supercronic", "/etc/crontab"] }] - `{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 - `__. + .. seealso:: + + :ref:`templates` for information on fields available in template strings. + """, ) @@ -2330,8 +2299,9 @@ def get_state(self): """ state = super().get_state() state["kubespawner_version"] = __version__ - # these should only be persisted if our pod is running + # pod_name, dns_name should only be persisted if our pod is running # but we don't have a sync check for that + # is that true for namespace as well? (namespace affects pvc) state['pod_name'] = self.pod_name state['namespace'] = self.namespace state['dns_name'] = self.dns_name @@ -2415,13 +2385,11 @@ def clear_state(self): super().clear_state() # this should be the same initialization as __init__ / trait defaults # this allows changing config to take effect after a server restart - if self.enable_user_namespaces: - self.namespace = self._expand_user_properties(self.user_namespace_template) - 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 ) + # reset namespace as well? async def poll(self): """ diff --git a/tests/test_slugs.py b/tests/test_slugs.py index e4845c56..bad39885 100644 --- a/tests/test_slugs.py +++ b/tests/test_slugs.py @@ -53,7 +53,7 @@ def test_safe_slug_max_length(max_length, length, expected): ("x", "x"), ("-x", "x---a4209624"), ("x-", "x---c8b60efc"), - pytest.param("x" * 63, "x" * 63, id="x64"), + pytest.param("x" * 63, "x" * 63, id="x63"), pytest.param("x" * 64, "xxxxxxxxxxxxxxxxxxxxx---7ce10097", id="x64"), pytest.param("x" * 65, "xxxxxxxxxxxxxxxxxxxxx---9537c5fd", id="x65"), ],