Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Tenant ID #1097

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
20 changes: 20 additions & 0 deletions docs/source/contributors/system-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,23 @@ Theoretically speaking, enabling a kernel for use in other frameworks amounts to
```{seealso}
This topic is covered in the [Developers Guide](../developers/index.rst).
```

## Multiple Tenant Support

Enterprise Gateway offers viably minimal support multi-tenant environments by tracking the managed kernels by their tenant _ID_. This is accomplished on the client request when starting a kernel by adding a UUID-formatted string to the kernel start request's body with an associated key of `tenant_id`.

```JSON
{"tenant_id":"f730794d-d175-40fa-b819-2a67d5308210"}
```

Likewise, when calling the `/api/kernels` endpoint to get the list of active kernels, tenant-aware applications should add a `tenant_id` query parameter in order to get appropriate managed kernel information.

```
GET /api/kernels?tenant_id=f730794d-d175-40fa-b819-2a67d5308210
```

Kernel start or list requests that do not include a `tenant_id` will have their kernels associated with the `UNIVERSAL_TENANT_ID` which merely acts a catch-all and allows common code usage relative to existing clients.

Enterprise Gateway will add the environment variable `KERNEL_TENANT_ID` to the kernel's environment so that this value is available to the kernel's launch logic and the kernel itself. It should be noted that if the original request also included a `KERNEL_TENANT_ID` in the body's `env` stanza, it will be overwritten with the value corresponding to `tenant_id` (or `UNIVERSAL_TENANT_ID` if `tenant_id` was not provided).
kevin-bates marked this conversation as resolved.
Show resolved Hide resolved

Kernel specifications and other resources do not currently adhere to tenant-based _partitioning_.
9 changes: 9 additions & 0 deletions docs/source/users/kernel-envs.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ There are several supported `KERNEL_` variables that the Enterprise Gateway serv
(https://googlecloudplatform.github.io/spark-on-k8s-operator/docs/api-docs.html#sparkoperator.k8s.io/v1beta2.SparkApplicationSpec)
sparkConfigMap for more information.

KERNEL_TENANT_ID=<system provided>
Indicates the tenant ID (UUID string) corresponding to the kernel. This value
is derived from the optional `tenant_id` provided by the client application and
is meant to represent the entity or organization in which the client application
is associated. If `tenant_id` is not provided on the kernel start request, then
`KERNEL_TENANT_ID` will hold a value of ``"27182818-2845-9045-2353-602874713527"`
(the `UNIVERSAL_TENANT_ID`), which provides for backwards compatible support for
older applications.

KERNEL_UID=<from user> or 1000
Containers only. This value represents the user id in which the container will run.
The default value is 1000 representing the jovyan user - which is how all kernel images
Expand Down
15 changes: 14 additions & 1 deletion enterprise_gateway/services/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ paths:
summary: List the JSON data for all currently running kernels
tags:
- kernels
parameters:
- name: tenant_id
in: query
description: When present, the list of running kernels will apply to the given tenant ID.
required: false
type: string
format: uuid
responses:
200:
description: List of running kernels
Expand All @@ -137,11 +144,17 @@ paths:
name:
type: string
description: Kernel spec name (defaults to default kernel spec for server)
tenant_id:
type: string
format: uuid
description: |
The (optional) UUID of the tenant making the request. The list of active
(running) kernels will be filtered by this value.
env:
type: object
description: |
A dictionary of environment variables and values to include in the
kernel process - subject to whitelisting.
kernel process - subject to filtering.
additionalProperties:
type: string
responses:
Expand Down
76 changes: 46 additions & 30 deletions enterprise_gateway/services/kernels/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import jupyter_server.services.kernels.handlers as jupyter_server_handlers
import tornado
from jupyter_client.jsonutil import date_default
from jupyter_server.utils import ensure_async
from tornado import web

from ...mixins import CORSMixin, JSONErrorsMixin, TokenAuthorizationMixin
from ..kernels.remotemanager import UNIVERSAL_TENANT_ID


class MainKernelHandler(
Expand Down Expand Up @@ -45,35 +47,40 @@ async def post(self):
if len(kernels) >= max_kernels:
raise tornado.web.HTTPError(403, "Resource Limit")

# Try to get env vars from the request body
env = {}
# Try to get tenant_id and env vars from the request body
model = self.get_json_body()
if model is not None and "env" in model:
if not isinstance(model["env"], dict):
raise tornado.web.HTTPError(400)
# Start with the PATH from the current env. Do not provide the entire environment
# which might contain server secrets that should not be passed to kernels.
env = {"PATH": os.getenv("PATH", "")}
# Whitelist environment variables from current process environment
env.update(
{
key: value
for key, value in os.environ.items()
if key in self.env_process_whitelist
}
)
# Whitelist KERNEL_* args and those allowed by configuration from client. If all
# envs are requested, just use the keys from the payload.
env_whitelist = self.env_whitelist
if env_whitelist == ["*"]:
env_whitelist = model["env"].keys()
env.update(
{
key: value
for key, value in model["env"].items()
if key.startswith("KERNEL_") or key in env_whitelist
}
)
if model is not None:
tenant_id = model.get("tenant_id", UNIVERSAL_TENANT_ID)
kevin-bates marked this conversation as resolved.
Show resolved Hide resolved
if "env" in model:
if not isinstance(model["env"], dict):
raise tornado.web.HTTPError(400)
# Start with the PATH from the current env. Do not provide the entire environment
# which might contain server secrets that should not be passed to kernels.
env = {"PATH": os.getenv("PATH", "")}
# Whitelist environment variables from current process environment
env.update(
{
key: value
for key, value in os.environ.items()
if key in self.env_process_whitelist
}
)
# Whitelist KERNEL_* args and those allowed by configuration from client. If all
# envs are requested, just use the keys from the payload.
env_whitelist = self.env_whitelist
if env_whitelist == ["*"]:
env_whitelist = model["env"].keys()
env.update(
{
key: value
for key, value in model["env"].items()
if key.startswith("KERNEL_") or key in env_whitelist
}
)

# Set KERNEL_TENANT_ID. If already present, we override with the value in the body
env["KERNEL_TENANT_ID"] = tenant_id
# If kernel_headers are configured, fetch each of those and include in start request
kernel_headers = {}
missing_headers = []
Expand All @@ -97,7 +104,10 @@ async def post(self):
# so do a temporary partial (ugh)
orig_start = self.kernel_manager.start_kernel
self.kernel_manager.start_kernel = partial(
self.kernel_manager.start_kernel, env=env, kernel_headers=kernel_headers
self.kernel_manager.start_kernel,
env=env,
kernel_headers=kernel_headers,
tenant_id=tenant_id,
kevin-bates marked this conversation as resolved.
Show resolved Hide resolved
)
try:
await super().post()
Expand All @@ -117,10 +127,16 @@ async def get(self):
tornado.web.HTTPError
403 Forbidden if kernel listing is disabled
"""
if not self.settings.get("eg_list_kernels"):

tenant_id_filter = self.request.query_arguments.get("tenant_id") or UNIVERSAL_TENANT_ID
if isinstance(tenant_id_filter, list):
tenant_id_filter = tenant_id_filter[0].decode("utf-8")
if not self.settings.get("eg_list_kernels") and tenant_id_filter == UNIVERSAL_TENANT_ID:
kevin-bates marked this conversation as resolved.
Show resolved Hide resolved
raise tornado.web.HTTPError(403, "Forbidden")
else:
await super().get()
km = self.kernel_manager
kernels = await ensure_async(km.list_kernels(tenant_id=tenant_id_filter))
self.finish(json.dumps(kernels, default=date_default))

def options(self, **kwargs):
"""Method for properly handling CORS pre-flight"""
Expand Down
Loading