Repository: jupyterhub/zero-to-jupyterhub-k8s

Issue:
Autogenerate and set passwords for JupyterHub's registered services' api_tokens
### Proposed change

To help charts like dask-gateway and binderhub that depend on z2jh to no longer need to explicitly set an api_token via `jupyterhub.hub.services.dask-gateway.api_token` for example. To instead let them read an automatically created api_token for their service via a k8s Secret.

This is very similar to the pr to seed this Helm chart's internally used tokens as was done in #1993.

### Alternative options

To do nothing and to still manually require creating and managing a secret api_token for a JupyterHub registered service, which also needs to be passed to the service in undefined ways for trusted communication between the service and JupyterHub.

This is currently done like this for dask-gateway for example, as described [in its README.md file](https://github.com/dask/helm-chart/tree/main/daskhub#prepare-configuration-file).

```yaml
jupyterhub:
  hub:
    services:
      dask-gateway:
        apiToken: "<secret-token>"

dask-gateway:
  gateway:
    auth:
      jupyterhub:
        apiToken: "<secret-token>"
```

### Who would use this feature?

Any user of binderhub, dask-gateway, and other Helm charts as those currently require configuration like above. With this feature, those chart can make it not needed.

### Implementation ideas

No matter what, we should have a way to expose the api_token of registered services, but this is already done.

- [x] We automatically declare api_tokens in the hub k8s Secret under a key named `hub.services.<service-name>.apiToken` in the k8s Secret typically named `hub` but more generally named in a way that can be dynamically set via `{{ include "jupyterhub.hub.fullname" . }}`.
  ```yaml
  data:
    # Any JupyterHub Services api_tokens are exposed in this k8s Secret as a
    # convinience for external services running in the k8s cluster that could
    # mount them directly from this k8s Secret.
    {{- range $key, $service := .Values.hub.services }}
    {{- if or $service.api_token $service.apiToken }}
    hub.services.{{ $service.name | default $key }}.apiToken: {{ $service.api_token | default $service.apiToken | b64enc | quote }}
    {{- end }}
    {{- end }}
  ```

#### Strategy 1 - specific automation of generating hub.services' api_token (#2312)

- [x] We automatically create an api_token for all registered JupyterHub services that doesn't explicitly set one.
- [x] We document that we do this, and give an example on how that api_token can be accessed via the hub k8s Secret.

#### Strategy 2 - generic password generation and api_token configuration

- [ ] We create an option to generate passwords to be stored in a k8s Secret
- [ ] We create an option for api_tokens to reference the value in a k8s Secret's specific key



Discussion:

Resolving patch:
diff --git a/jupyterhub/files/hub/jupyterhub_config.py b/jupyterhub/files/hub/jupyterhub_config.py
--- a/jupyterhub/files/hub/jupyterhub_config.py
+++ b/jupyterhub/files/hub/jupyterhub_config.py
@@ -359,17 +359,15 @@ def camelCaseify(s):
         }
     )
 
-for name, service in get_config("hub.services", {}).items():
+for key, service in get_config("hub.services", {}).items():
     # c.JupyterHub.services is a list of dicts, but
     # hub.services is a dict of dicts to make the config mergable
-    service.setdefault("name", name)
+    service.setdefault("name", key)
 
     # As the api_token could be exposed in hub.existingSecret, we need to read
     # it it from there or fall back to the chart managed k8s Secret's value.
     service.pop("apiToken", None)
-    api_token = get_secret_value(f"hub.services.{service['name']}.apiToken", None)
-    if api_token:
-        service["api_token"] = api_token
+    service["api_token"] = get_secret_value(f"hub.services.{key}.apiToken")
 
     c.JupyterHub.services.append(service)
 


Test patch: