From 43032d6699217877b2f7b6ca0595761fb8174986 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Mon, 18 Jul 2022 13:44:18 -0700 Subject: [PATCH 1/4] Deprecate env-whitelist traits, send client-envs in kernelspec --- enterprise_gateway/mixins.py | 45 ++++++++++++++----- .../services/kernelspecs/handlers.py | 12 ++--- .../services/kernelspecs/kernelspec_cache.py | 44 ++++++++++++++---- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/enterprise_gateway/mixins.py b/enterprise_gateway/mixins.py index d7128f84c..f973b119d 100644 --- a/enterprise_gateway/mixins.py +++ b/enterprise_gateway/mixins.py @@ -372,30 +372,53 @@ def list_kernels_default(self) -> bool: == "true" ) - env_whitelist_env = "EG_ENV_WHITELIST" env_whitelist = ListTrait( config=True, - help="""Environment variables allowed to be set when a client requests a - new kernel. Use '*' to allow all environment variables sent in the request. - (EG_ENV_WHITELIST env var)""", + help="""DEPRECATED, use allowed_envs.""", ) @default("env_whitelist") def env_whitelist_default(self) -> List[str]: return os.getenv(self.env_whitelist_env, os.getenv("KG_ENV_WHITELIST", "")).split(",") - env_process_whitelist_env = "EG_ENV_PROCESS_WHITELIST" + @observe("env_whitelist") + def _update_env_whitelist(self, change): + self.log.warning("env_whitelist is deprecated, use client_envs") + self.client_envs = change["new"] + + client_envs_env = "EG_CLIENT_ENVS" + client_envs = ListTrait( + config=True, + help="""Environment variables allowed to be set when a client requests a + new kernel. (EG_CLIENT_ENVS env var)""", + ) + + @default("client_envs") + def client_envs_default(self): + return os.getenv(self.client_envs_env, os.getenv("EG_ENV_WHITELIST", "")).split(",") + env_process_whitelist = ListTrait( + config=True, + help="""DEPRECATED, use inherited_envs""", + ) + + @observe("env_process_whitelist") + def _update_env_process_whitelist(self, change): + self.log.warning("env_process_whitelist is deprecated, use inherited_envs") + self.inherited_envs = change["new"] + + inherited_envs_env = "EG_INHERITED_ENVS" + inherited_envs = ListTrait( config=True, help="""Environment variables allowed to be inherited - from the spawning process by the kernel. (EG_ENV_PROCESS_WHITELIST env var)""", + from the spawning process by the kernel. (EG_INHERITED_ENVS env var)""", ) - @default("env_process_whitelist") - def env_process_whitelist_default(self) -> List[str]: - return os.getenv( - self.env_process_whitelist_env, os.getenv("KG_ENV_PROCESS_WHITELIST", "") - ).split(",") + @default("inherited_envs") + def inherited_envs_default(self) -> List[str]: + return os.getenv(self.inherited_envs_env, os.getenv("EG_ENV_PROCESS_WHITELIST", "")).split( + "," + ) kernel_headers_env = "EG_KERNEL_HEADERS" kernel_headers = ListTrait( diff --git a/enterprise_gateway/services/kernelspecs/handlers.py b/enterprise_gateway/services/kernelspecs/handlers.py index b334765b5..c1aa80cbb 100644 --- a/enterprise_gateway/services/kernelspecs/handlers.py +++ b/enterprise_gateway/services/kernelspecs/handlers.py @@ -69,7 +69,7 @@ def kernel_spec_cache(self) -> KernelSpecCache: @web.authenticated async def get(self) -> None: - ksm = self.kernel_spec_cache + ksc = self.kernel_spec_cache km = self.kernel_manager model = {} model["default"] = km.default_kernel_name @@ -82,7 +82,7 @@ async def get(self) -> None: if kernel_user: self.log.debug("Searching kernels for user '%s' " % kernel_user) - kspecs = await ensure_async(ksm.get_all_specs()) + kspecs = await ensure_async(ksc.get_all_specs()) list_kernels_found = [] for kernel_name, kernel_info in kspecs.items(): @@ -122,14 +122,14 @@ def kernel_spec_cache(self) -> KernelSpecCache: @web.authenticated async def get(self, kernel_name: str) -> None: - ksm = self.kernel_spec_cache + ksc = self.kernel_spec_cache kernel_name = url_unescape(kernel_name) kernel_user_filter = self.request.query_arguments.get("user") kernel_user = None if kernel_user_filter: kernel_user = kernel_user_filter[0].decode("utf-8") try: - spec = await ensure_async(ksm.get_kernel_spec(kernel_name)) + spec = await ensure_async(ksc.get_kernel_spec(kernel_name)) except KeyError: raise web.HTTPError(404, "Kernel spec %s not found" % kernel_name) if is_kernelspec_model(spec): @@ -166,9 +166,9 @@ def initialize(self) -> None: @web.authenticated async def get(self, kernel_name: str, path: str, include_body: bool = True) -> None: - ksm = self.kernel_spec_cache + ksc = self.kernel_spec_cache try: - kernelspec = await ensure_async(ksm.get_kernel_spec(kernel_name)) + kernelspec = await ensure_async(ksc.get_kernel_spec(kernel_name)) self.root = kernelspec.resource_dir except KeyError as e: raise web.HTTPError(404, "Kernel spec %s not found" % kernel_name) from e diff --git a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py index 19c51a45e..c114f555c 100644 --- a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py +++ b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py @@ -3,7 +3,7 @@ """Cache handling for kernel specs.""" import os -from typing import Dict, Optional, Union +from typing import Dict, Optional, Set, Union from jupyter_client.kernelspec import KernelSpec from jupyter_server.utils import ensure_async @@ -33,7 +33,7 @@ class KernelSpecCache(SingletonConfigurable): cache_enabled_env = "EG_KERNELSPEC_CACHE_ENABLED" cache_enabled = CBool( - False, + True, config=True, help="""Enable Kernel Specification caching. (EG_KERNELSPEC_CACHE_ENABLED env var)""", ) @@ -110,7 +110,7 @@ def get_item(self, kernel_name: str) -> Optional[KernelSpec]: ) return kernelspec - def get_all_items(self) -> Optional[Dict[str, CacheItemType]]: + def get_all_items(self) -> Dict[str, CacheItemType]: """Retrieves all kernel specification from the cache. If cache is disabled or no items are in the cache, an empty dictionary is returned; @@ -140,6 +140,34 @@ def put_item(self, kernel_name: str, cache_item: Union[KernelSpec, CacheItemType kernel_name=kernel_name ) ) + # Irrespective of cache enablement, add/update the 'metadata.client_envs' entry + # with the set of configured values. If the stanza already exists in the kernelspec + # update with the union of it and those values configured via `EnterpriseGatewayApp'. + # We apply this logic here so that its only performed once for cached values or on + # every retrieval when caching is not enabled. + # Note: We only need to do this if we have a KernelSpec instance, since CacheItemType + # instances will have already been modified. + + # Create a set from the configured value, update it with the (potential) value + # in the kernelspec, and apply the changes back to the kernelspec. + + client_envs: Set[str] = set(self.parent.client_envs) + kspec_client_envs: Set[str] + if type(cache_item) is KernelSpec: + kspec: KernelSpec = cache_item + kspec_client_envs = set(kspec.metadata.get("client_envs", [])) + else: + kspec_client_envs = set(cache_item["spec"].get("metadata", {}).get("client_envs", [])) + + client_envs.update(kspec_client_envs) + if type(cache_item) is KernelSpec: + kspec: KernelSpec = cache_item + kspec.metadata["client_envs"] = list(client_envs) + else: + if "metadata" not in cache_item["spec"]: + cache_item["spec"]["metadata"] = {} + cache_item["spec"]["metadata"]["client_envs"] = list(client_envs) + if self.cache_enabled: if type(cache_item) is KernelSpec: cache_item = KernelSpecCache.kernel_spec_to_cache_item(cache_item) @@ -159,9 +187,8 @@ def put_item(self, kernel_name: str, cache_item: Union[KernelSpec, CacheItemType def put_all_items(self, kernelspecs: Dict[str, CacheItemType]) -> None: """Adds or updates a dictionary of kernel specification in the cache.""" - if self.cache_enabled and kernelspecs: - for kernel_name, cache_item in kernelspecs.items(): - self.put_item(kernel_name, cache_item) + for kernel_name, cache_item in kernelspecs.items(): + self.put_item(kernel_name, cache_item) def remove_item(self, kernel_name: str) -> Optional[CacheItemType]: """Removes the cache item corresponding to kernel_name from the cache.""" @@ -212,7 +239,7 @@ def _initialize(self): @staticmethod def kernel_spec_to_cache_item(kernelspec: KernelSpec) -> CacheItemType: - """Convets a KernelSpec instance to a CacheItemType for storage into the cache.""" + """Converts a KernelSpec instance to a CacheItemType for storage into the cache.""" cache_item = dict() cache_item["spec"] = kernelspec.to_dict() cache_item["resource_dir"] = kernelspec.resource_dir @@ -221,7 +248,8 @@ def kernel_spec_to_cache_item(kernelspec: KernelSpec) -> CacheItemType: @staticmethod def cache_item_to_kernel_spec(cache_item: CacheItemType) -> KernelSpec: """Converts a CacheItemType to a KernelSpec instance for user consumption.""" - return KernelSpec.from_resource_dir(cache_item["resource_dir"]) + kernel_spec = KernelSpec(resource_dir=cache_item["resource_dir"], **cache_item["spec"]) + return kernel_spec class KernelSpecChangeHandler(FileSystemEventHandler): From 57395b029ddf81f3cca01243a2f3e26efbbea044 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 26 Jul 2022 11:18:48 -0700 Subject: [PATCH 2/4] Address changes in docs and deployment scripts --- docs/source/developers/custom-images.md | 2 +- docs/source/operators/config-cli.md | 15 ++++++++++----- .../source/operators/config-kernel-override.md | 6 ++++-- enterprise_gateway/enterprisegatewayapp.py | 4 ++-- enterprise_gateway/mixins.py | 6 +----- enterprise_gateway/services/api/swagger.json | 2 +- enterprise_gateway/services/api/swagger.yaml | 2 +- .../services/kernels/handlers.py | 18 +++++++----------- .../services/kernels/remotemanager.py | 8 ++++---- .../services/processproxies/k8s.py | 2 +- enterprise_gateway/tests/test_handlers.py | 5 +++-- etc/docker/docker-compose.yml | 3 ++- .../start-enterprise-gateway.sh | 10 +++++----- .../docker/scripts/launch_docker.py | 2 +- .../templates/deployment.yaml | 4 ++-- .../helm/enterprise-gateway/values.yaml | 4 ++-- 16 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/source/developers/custom-images.md b/docs/source/developers/custom-images.md index b70d624d2..62b2a3e88 100644 --- a/docs/source/developers/custom-images.md +++ b/docs/source/developers/custom-images.md @@ -164,6 +164,6 @@ cp -r python_kubernetes python_myCustomKernel } ``` -- If using a whitelist (`EG_KERNEL_WHITELIST`), be sure to update it with the new kernel specification directory name (e.g., `python_myCustomKernel`) and restart/redeploy Enterprise Gateway. +- If using kernel filtering (`EG_ALLOWED_KERNELS`), be sure to update it with the new kernel specification directory name (e.g., `python_myCustomKernel`) and restart/redeploy Enterprise Gateway. - Launch or refresh your Notebook session and confirm `My Custom Kernel` appears in the _new kernel_ drop-down. - Create a new notebook using `My Custom Kernel`. diff --git a/docs/source/operators/config-cli.md b/docs/source/operators/config-cli.md index a226fc712..c3c64069c 100644 --- a/docs/source/operators/config-cli.md +++ b/docs/source/operators/config-cli.md @@ -121,6 +121,10 @@ EnterpriseGatewayApp(EnterpriseGatewayConfigMixin, JupyterApp) options The full path to a certificate authority certificate for SSL/TLS client authentication. (EG_CLIENT_CA env var) Default: None +--EnterpriseGatewayApp.client_envs=... + Environment variables allowed to be set when a client requests a + new kernel. (EG_CLIENT_ENVS env var) + Default: [] --EnterpriseGatewayApp.conductor_endpoint= The http url for accessing the Conductor REST API. (EG_CONDUCTOR_ENDPOINT env var) @@ -140,13 +144,10 @@ EnterpriseGatewayApp(EnterpriseGatewayConfigMixin, JupyterApp) options (EG_DYNAMIC_CONFIG_INTERVAL env var) Default: 0 --EnterpriseGatewayApp.env_process_whitelist=... - Environment variables allowed to be inherited from the spawning process by - the kernel. (EG_ENV_PROCESS_WHITELIST env var) + DEPRECATED, use inherited_envs Default: [] --EnterpriseGatewayApp.env_whitelist=... - Environment variables allowed to be set when a client requests a new kernel. - Use '*' to allow all environment variables sent in the request. - (EG_ENV_WHITELIST env var) + DEPRECATED, use client_envs. Default: [] --EnterpriseGatewayApp.expose_headers= Sets the Access-Control-Expose-Headers header. (EG_EXPOSE_HEADERS env var) @@ -158,6 +159,10 @@ EnterpriseGatewayApp(EnterpriseGatewayConfigMixin, JupyterApp) options Indicates whether impersonation will be performed during kernel launch. (EG_IMPERSONATION_ENABLED env var) Default: False +--EnterpriseGatewayApp.inherited_envs=... + Environment variables allowed to be inherited + from the spawning process by the kernel. (EG_INHERITED_ENVS env var) + Default: [] --EnterpriseGatewayApp.ip= IP address on which to listen (EG_IP env var) Default: '127.0.0.1' diff --git a/docs/source/operators/config-kernel-override.md b/docs/source/operators/config-kernel-override.md index e9599d362..9882c69dc 100644 --- a/docs/source/operators/config-kernel-override.md +++ b/docs/source/operators/config-kernel-override.md @@ -38,8 +38,10 @@ those same-named variables in the kernel.json `env` stanza. Environment variables for which this can occur are any variables prefixed with `KERNEL_` as well as any variables -listed in the `EnterpriseGatewayApp.env_whitelist` configurable trait (or via -the `EG_ENV_WHITELIST` variable). Locally defined variables listed in `EG_PROCESS_ENV_WHITELIST` +listed in the `EnterpriseGatewayApp.client_envs` configurable trait (or via +the `EG_CLIENT_ENVS` variable). Likewise, environment variables of the Enterprise Gateway +server process listed in the `EnterpriseGatewayApp.inherited_envs` configurable trait +(or via the `EG_INHERITED_ENVS` variable) are also available for replacement in the kernel process' environment. See [Kernel Environment Variables](../users/kernel-envs.md) in the Users documentation section for a complete set of recognized `KERNEL_` variables. diff --git a/enterprise_gateway/enterprisegatewayapp.py b/enterprise_gateway/enterprisegatewayapp.py index 5ca11ae02..1304565ca 100644 --- a/enterprise_gateway/enterprisegatewayapp.py +++ b/enterprise_gateway/enterprisegatewayapp.py @@ -236,8 +236,8 @@ def init_webapp(self) -> None: eg_expose_headers=self.expose_headers, eg_max_age=self.max_age, eg_max_kernels=self.max_kernels, - eg_env_process_whitelist=self.env_process_whitelist, - eg_env_whitelist=self.env_whitelist, + eg_inherited_envs=self.inherited_envs, + eg_client_envs=self.client_envs, eg_kernel_headers=self.kernel_headers, eg_list_kernels=self.list_kernels, eg_authorized_users=self.authorized_users, diff --git a/enterprise_gateway/mixins.py b/enterprise_gateway/mixins.py index f973b119d..4adf91cb4 100644 --- a/enterprise_gateway/mixins.py +++ b/enterprise_gateway/mixins.py @@ -374,13 +374,9 @@ def list_kernels_default(self) -> bool: env_whitelist = ListTrait( config=True, - help="""DEPRECATED, use allowed_envs.""", + help="""DEPRECATED, use client_envs.""", ) - @default("env_whitelist") - def env_whitelist_default(self) -> List[str]: - return os.getenv(self.env_whitelist_env, os.getenv("KG_ENV_WHITELIST", "")).split(",") - @observe("env_whitelist") def _update_env_whitelist(self, change): self.log.warning("env_whitelist is deprecated, use client_envs") diff --git a/enterprise_gateway/services/api/swagger.json b/enterprise_gateway/services/api/swagger.json index a32d205d7..3a62926eb 100644 --- a/enterprise_gateway/services/api/swagger.json +++ b/enterprise_gateway/services/api/swagger.json @@ -160,7 +160,7 @@ }, "env": { "type": "object", - "description": "A dictionary of environment variables and values to include in the kernel process - subject to whitelisting.", + "description": "A dictionary of environment variables and values to include in the kernel process - subject to filtering.", "additionalProperties": { "type": "string" } diff --git a/enterprise_gateway/services/api/swagger.yaml b/enterprise_gateway/services/api/swagger.yaml index 5e5ea1c05..2ae8611a1 100644 --- a/enterprise_gateway/services/api/swagger.yaml +++ b/enterprise_gateway/services/api/swagger.yaml @@ -141,7 +141,7 @@ paths: 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: diff --git a/enterprise_gateway/services/kernels/handlers.py b/enterprise_gateway/services/kernels/handlers.py index 5bdcf79b8..4e1859e16 100644 --- a/enterprise_gateway/services/kernels/handlers.py +++ b/enterprise_gateway/services/kernels/handlers.py @@ -22,12 +22,12 @@ class MainKernelHandler( """ @property - def env_whitelist(self): - return self.settings["eg_env_whitelist"] + def client_envs(self): + return self.settings["eg_client_envs"] @property - def env_process_whitelist(self): - return self.settings["eg_env_process_whitelist"] + def inherited_envs(self): + return self.settings["eg_inherited_envs"] async def post(self): """Overrides the super class method to manage env in the request body. @@ -59,19 +59,15 @@ async def post(self): { key: value for key, value in os.environ.items() - if key in self.env_process_whitelist + if key in self.inherited_envs } ) - # 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() + # Allow KERNEL_* args and those allowed by configuration. env.update( { key: value for key, value in model["env"].items() - if key.startswith("KERNEL_") or key in env_whitelist + if key.startswith("KERNEL_") or key in self.client_envs } ) diff --git a/enterprise_gateway/services/kernels/remotemanager.py b/enterprise_gateway/services/kernels/remotemanager.py index a53ce8e90..8f68fff96 100644 --- a/enterprise_gateway/services/kernels/remotemanager.py +++ b/enterprise_gateway/services/kernels/remotemanager.py @@ -427,8 +427,8 @@ def _link_dependent_props(self): "port_range", "impersonation_enabled", "max_kernels_per_user", - "env_whitelist", - "env_process_whitelist", + "client_envs", + "inherited_envs", "yarn_endpoint", "alt_yarn_endpoint", "yarn_endpoint_security_enabled", @@ -470,8 +470,8 @@ def _capture_user_overrides(self, **kwargs): key: value for key, value in env.items() if key.startswith("KERNEL_") - or key in self.env_process_whitelist - or key in self.env_whitelist + or key in self.inherited_envs + or key in self.client_envs } ) diff --git a/enterprise_gateway/services/processproxies/k8s.py b/enterprise_gateway/services/processproxies/k8s.py index b51e8bf47..2fe4d227c 100644 --- a/enterprise_gateway/services/processproxies/k8s.py +++ b/enterprise_gateway/services/processproxies/k8s.py @@ -53,7 +53,7 @@ async def launch_process( # transfer its env to each launched kernel. kwargs["env"] = dict( os.environ, **kwargs["env"] - ) # FIXME: Should probably use process-whitelist in JKG #280 + ) self.kernel_pod_name = self._determine_kernel_pod_name(**kwargs) self.kernel_namespace = self._determine_kernel_namespace( **kwargs diff --git a/enterprise_gateway/tests/test_handlers.py b/enterprise_gateway/tests/test_handlers.py index 9342b37a5..aeaef2295 100644 --- a/enterprise_gateway/tests/test_handlers.py +++ b/enterprise_gateway/tests/test_handlers.py @@ -24,11 +24,12 @@ def setup_app(self): os.environ["JUPYTER_PATH"] = RESOURCES # These are required for setup of test_kernel_defaults + # Note: We still reference the DEPRECATED config parameter and environment variable so that + # we can test client_envs and inherited_envs, respectively. + self.app.env_whitelist = ['TEST_VAR', 'OTHER_VAR1', 'OTHER_VAR2'] os.environ["EG_ENV_PROCESS_WHITELIST"] = "PROCESS_VAR1,PROCESS_VAR2" os.environ["PROCESS_VAR1"] = "process_var1_override" - self.app.env_whitelist = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] - def tearDown(self): """Shuts down the app after test run.""" diff --git a/etc/docker/docker-compose.yml b/etc/docker/docker-compose.yml index 6a96148db..5a17c5ae3 100644 --- a/etc/docker/docker-compose.yml +++ b/etc/docker/docker-compose.yml @@ -22,7 +22,8 @@ services: - "EG_DOCKER_NETWORK=${EG_DOCKER_NETWORK:-enterprise-gateway_enterprise-gateway}" - "EG_KERNEL_LAUNCH_TIMEOUT=${EG_KERNEL_LAUNCH_TIMEOUT:-60}" - "EG_CULL_IDLE_TIMEOUT=${EG_CULL_IDLE_TIMEOUT:-3600}" - - "EG_KERNEL_WHITELIST=${EG_KERNEL_WHITELIST:-'r_docker','python_docker','python_tf_docker','python_tf_gpu_docker','scala_docker'}" + # Use double-defaulting for B/C. Support for EG_KERNEL_WHITELIST will be removed in a future release + - "EG_ALLOWED_KERNELS=${EG_ALLOWED_KERNELS:-${EG_KERNEL_WHITELIST:-'r_docker','python_docker','python_tf_docker','python_tf_gpu_docker','scala_docker'}}" - "EG_MIRROR_WORKING_DIRS=${EG_MIRROR_WORKING_DIRS:-False}" - "EG_RESPONSE_PORT=${EG_RESPONSE_PORT:-8877}" - "KG_PORT=${KG_PORT:-8888}" diff --git a/etc/docker/enterprise-gateway/start-enterprise-gateway.sh b/etc/docker/enterprise-gateway/start-enterprise-gateway.sh index d1e6a124c..1ad25bc1f 100755 --- a/etc/docker/enterprise-gateway/start-enterprise-gateway.sh +++ b/etc/docker/enterprise-gateway/start-enterprise-gateway.sh @@ -26,19 +26,19 @@ export EG_LOG_LEVEL=${EG_LOG_LEVEL:-DEBUG} export EG_CULL_IDLE_TIMEOUT=${EG_CULL_IDLE_TIMEOUT:-43200} # default to 12 hours export EG_CULL_INTERVAL=${EG_CULL_INTERVAL:-60} export EG_CULL_CONNECTED=${EG_CULL_CONNECTED:-False} -EG_KERNEL_WHITELIST=${EG_KERNEL_WHITELIST:-"null"} -export EG_KERNEL_WHITELIST=`echo ${EG_KERNEL_WHITELIST} | sed 's/[][]//g'` # sed is used to strip off surrounding brackets as they should no longer be included. +EG_ALLOWED_KERNELS=${EG_ALLOWED_KERNELS:-${EG_KERNEL_WHITELIST:-"null"}} +export EG_ALLOWED_KERNELS=`echo ${EG_ALLOWED_KERNELS} | sed 's/[][]//g'` # sed is used to strip off surrounding brackets as they should no longer be included. export EG_DEFAULT_KERNEL_NAME=${EG_DEFAULT_KERNEL_NAME:-python_docker} # Determine whether the kernels-allowed list should be added to the start command. # This is conveyed via a 'null' value for the env - which indicates no kernel names # were used in the helm chart or docker-compose yaml. allowed_kernels_option="" -if [ "${EG_KERNEL_WHITELIST}" != "null" ]; then - allowed_kernels_option="--KernelSpecManager.whitelist=[${EG_KERNEL_WHITELIST}]" +if [ "${EG_ALLOWED_KERNELS}" != "null" ]; then + # Update to --KernelSpecManager.allowed_kernelspecs once jupyter_client >= 7 can be supported + allowed_kernels_option="--KernelSpecManager.whitelist=[${EG_ALLOWED_KERNELS}]" fi - echo "Starting Jupyter Enterprise Gateway..." exec jupyter enterprisegateway \ diff --git a/etc/kernel-launchers/docker/scripts/launch_docker.py b/etc/kernel-launchers/docker/scripts/launch_docker.py index a70f76c4c..bd505adaf 100644 --- a/etc/kernel-launchers/docker/scripts/launch_docker.py +++ b/etc/kernel-launchers/docker/scripts/launch_docker.py @@ -42,7 +42,7 @@ def launch_docker_kernel(kernel_id, port_range, response_addr, public_key, spark param_env["RESPONSE_ADDRESS"] = response_addr param_env["KERNEL_SPARK_CONTEXT_INIT_MODE"] = spark_context_init_mode - # Since the environment is specific to the kernel (per env stanza of kernelspec, KERNEL_ and ENV_WHITELIST) + # Since the environment is specific to the kernel (per env stanza of kernelspec, KERNEL_ and EG_CLIENT_ENVS) # just add the env here. param_env.update(os.environ) param_env.pop( diff --git a/etc/kubernetes/helm/enterprise-gateway/templates/deployment.yaml b/etc/kubernetes/helm/enterprise-gateway/templates/deployment.yaml index 473a6f198..ebdfeec50 100644 --- a/etc/kubernetes/helm/enterprise-gateway/templates/deployment.yaml +++ b/etc/kubernetes/helm/enterprise-gateway/templates/deployment.yaml @@ -69,8 +69,8 @@ spec: value: {{ .Values.logLevel }} - name: EG_KERNEL_LAUNCH_TIMEOUT value: !!str {{ .Values.kernel.launchTimeout }} - - name: EG_KERNEL_WHITELIST - value: {{ toJson .Values.kernel.whitelist | squote }} + - name: EG_ALLOWED_KERNELS + value: {{ toJson .Values.kernel.allowedKernels | squote }} - name: EG_DEFAULT_KERNEL_NAME value: {{ .Values.kernel.defaultKernelName }} # Optional authorization token passed in all requests diff --git a/etc/kubernetes/helm/enterprise-gateway/values.yaml b/etc/kubernetes/helm/enterprise-gateway/values.yaml index 0d5bb3250..e07702820 100644 --- a/etc/kubernetes/helm/enterprise-gateway/values.yaml +++ b/etc/kubernetes/helm/enterprise-gateway/values.yaml @@ -78,8 +78,8 @@ kernel: cullIdleTimeout: 3600 # List of kernel names that are available for use. To allow additional kernelspecs without # requiring redeployment (and assuming kernelspecs are mounted or otherwise accessible - # outside the pod), comment out (or remove) the entries, leaving only `whitelist:`. - whitelist: + # outside the pod), comment out (or remove) the entries, leaving only `allowedKernels:`. + allowedKernels: - r_kubernetes - python_kubernetes - python_tf_kubernetes From 269e4c2441e2ba2e7cd6b2c0013dc1d0b9db2784 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 26 Jul 2022 14:49:43 -0700 Subject: [PATCH 3/4] Update tests --- .../operators/config-kernel-override.md | 2 +- .../services/kernels/handlers.py | 6 +- .../services/kernelspecs/kernelspec_cache.py | 6 +- .../services/processproxies/k8s.py | 4 +- enterprise_gateway/tests/test_handlers.py | 84 +++++++++---------- .../tests/test_kernelspec_cache.py | 24 +++++- 6 files changed, 66 insertions(+), 60 deletions(-) diff --git a/docs/source/operators/config-kernel-override.md b/docs/source/operators/config-kernel-override.md index 9882c69dc..62947a695 100644 --- a/docs/source/operators/config-kernel-override.md +++ b/docs/source/operators/config-kernel-override.md @@ -41,7 +41,7 @@ as well as any variables listed in the `EnterpriseGatewayApp.client_envs` configurable trait (or via the `EG_CLIENT_ENVS` variable). Likewise, environment variables of the Enterprise Gateway server process listed in the `EnterpriseGatewayApp.inherited_envs` configurable trait -(or via the `EG_INHERITED_ENVS` variable) +(or via the `EG_INHERITED_ENVS` variable) are also available for replacement in the kernel process' environment. See [Kernel Environment Variables](../users/kernel-envs.md) in the Users documentation section for a complete set of recognized `KERNEL_` variables. diff --git a/enterprise_gateway/services/kernels/handlers.py b/enterprise_gateway/services/kernels/handlers.py index 4e1859e16..f3ffe0b0d 100644 --- a/enterprise_gateway/services/kernels/handlers.py +++ b/enterprise_gateway/services/kernels/handlers.py @@ -56,11 +56,7 @@ async def post(self): 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.inherited_envs - } + {key: value for key, value in os.environ.items() if key in self.inherited_envs} ) # Allow KERNEL_* args and those allowed by configuration. env.update( diff --git a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py index c114f555c..7ccb09c79 100644 --- a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py +++ b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py @@ -135,11 +135,7 @@ def put_item(self, kernel_name: str, cache_item: Union[KernelSpec, CacheItemType If it determines the cache entry corresponds to a currently unwatched directory, that directory will be added to list of observed directories and scheduled accordingly. """ - self.log.info( - "KernelSpecCache: adding/updating kernelspec: {kernel_name}".format( - kernel_name=kernel_name - ) - ) + self.log.info(f"KernelSpecCache: adding/updating kernelspec: {kernel_name}") # Irrespective of cache enablement, add/update the 'metadata.client_envs' entry # with the set of configured values. If the stanza already exists in the kernelspec # update with the union of it and those values configured via `EnterpriseGatewayApp'. diff --git a/enterprise_gateway/services/processproxies/k8s.py b/enterprise_gateway/services/processproxies/k8s.py index 2fe4d227c..e47058ea1 100644 --- a/enterprise_gateway/services/processproxies/k8s.py +++ b/enterprise_gateway/services/processproxies/k8s.py @@ -51,9 +51,7 @@ async def launch_process( # Kubernetes relies on many internal env variables. Since EG is running in a k8s pod, we will # transfer its env to each launched kernel. - kwargs["env"] = dict( - os.environ, **kwargs["env"] - ) + kwargs["env"] = dict(os.environ, **kwargs["env"]) self.kernel_pod_name = self._determine_kernel_pod_name(**kwargs) self.kernel_namespace = self._determine_kernel_namespace( **kwargs diff --git a/enterprise_gateway/tests/test_handlers.py b/enterprise_gateway/tests/test_handlers.py index aeaef2295..0302f5354 100644 --- a/enterprise_gateway/tests/test_handlers.py +++ b/enterprise_gateway/tests/test_handlers.py @@ -26,7 +26,7 @@ def setup_app(self): # These are required for setup of test_kernel_defaults # Note: We still reference the DEPRECATED config parameter and environment variable so that # we can test client_envs and inherited_envs, respectively. - self.app.env_whitelist = ['TEST_VAR', 'OTHER_VAR1', 'OTHER_VAR2'] + self.app.env_whitelist = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] os.environ["EG_ENV_PROCESS_WHITELIST"] = "PROCESS_VAR1,PROCESS_VAR2" os.environ["PROCESS_VAR1"] = "process_var1_override" @@ -644,44 +644,44 @@ def test_base_url(self): self.assertEqual(response.code, 200) -class TestWildcardEnvs(TestHandlers): - """Base class for jupyter-websocket mode tests that spawn kernels.""" - - def setup_app(self): - """Configure JUPYTER_PATH so that we can use local kernelspec files for testing.""" - super().setup_app() - # overwrite env_whitelist - self.app.env_whitelist = ["*"] - - @gen_test - def test_kernel_wildcard_env(self): - """Kernel should start with environment vars defined in the request.""" - # Note: Since env_whitelist == '*', all values should be present. - kernel_body = json.dumps( - { - "name": "python", - "env": { - "KERNEL_FOO": "kernel-foo-value", - "OTHER_VAR1": "other-var1-value", - "OTHER_VAR2": "other-var2-value", - "TEST_VAR": "test-var-value", - }, - } - ) - ws = yield self.spawn_kernel(kernel_body) - req = self.execute_request( - "import os; " - 'print(os.getenv("KERNEL_FOO"), ' - 'os.getenv("OTHER_VAR1"), ' - 'os.getenv("OTHER_VAR2"), ' - 'os.getenv("TEST_VAR"))' - ) - ws.write_message(json_encode(req)) - content = yield self.await_stream(ws) - self.assertEqual(content["name"], "stdout") - self.assertIn("kernel-foo-value", content["text"]) - self.assertIn("other-var1-value", content["text"]) - self.assertIn("other-var2-value", content["text"]) - self.assertIn("test-var-value", content["text"]) - - ws.close() +# class TestWildcardEnvs(TestHandlers): +# """Base class for jupyter-websocket mode tests that spawn kernels.""" +# +# def setup_app(self): +# """Configure JUPYTER_PATH so that we can use local kernelspec files for testing.""" +# super().setup_app() +# # overwrite env_whitelist +# self.app.env_whitelist = ["*"] +# +# @gen_test +# def test_kernel_wildcard_env(self): +# """Kernel should start with environment vars defined in the request.""" +# # Note: Since env_whitelist == '*', all values should be present. +# kernel_body = json.dumps( +# { +# "name": "python", +# "env": { +# "KERNEL_FOO": "kernel-foo-value", +# "OTHER_VAR1": "other-var1-value", +# "OTHER_VAR2": "other-var2-value", +# "TEST_VAR": "test-var-value", +# }, +# } +# ) +# ws = yield self.spawn_kernel(kernel_body) +# req = self.execute_request( +# "import os; " +# 'print(os.getenv("KERNEL_FOO"), ' +# 'os.getenv("OTHER_VAR1"), ' +# 'os.getenv("OTHER_VAR2"), ' +# 'os.getenv("TEST_VAR"))' +# ) +# ws.write_message(json_encode(req)) +# content = yield self.await_stream(ws) +# self.assertEqual(content["name"], "stdout") +# self.assertIn("kernel-foo-value", content["text"]) +# self.assertIn("other-var1-value", content["text"]) +# self.assertIn("other-var2-value", content["text"]) +# self.assertIn("test-var-value", content["text"]) +# +# ws.close() diff --git a/enterprise_gateway/tests/test_kernelspec_cache.py b/enterprise_gateway/tests/test_kernelspec_cache.py index 46a409c20..e27be1a61 100644 --- a/enterprise_gateway/tests/test_kernelspec_cache.py +++ b/enterprise_gateway/tests/test_kernelspec_cache.py @@ -12,6 +12,7 @@ import pytest from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel +from enterprise_gateway.enterprisegatewayapp import EnterpriseGatewayApp from enterprise_gateway.services.kernelspecs import KernelSpecCache @@ -60,10 +61,16 @@ def environ( # END - Remove once transition to jupyter_server occurs +GLOBAL_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] +KSPEC_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR3", "OTHER_VAR4"] +UNION_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2", "OTHER_VAR3", "OTHER_VAR4"] kernelspec_json = { "argv": ["cat", "{connection_file}"], "display_name": "Test kernel: {kernel_name}", + "metadata": { + "client_envs": KSPEC_CLIENT_ENVS, + } } @@ -103,17 +110,22 @@ def setup_kernelspecs(environ, kernelspec_location): @pytest.fixture def kernel_spec_manager(environ, setup_kernelspecs): - yield KernelSpecManager(ensure_native_kernel=False) + app = EnterpriseGatewayApp() + app.client_envs = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] + + yield KernelSpecManager(ensure_native_kernel=False, parent=app) @pytest.fixture def kernel_spec_cache(is_enabled, kernel_spec_manager): + kspec_cache = KernelSpecCache.instance( - kernel_spec_manager=kernel_spec_manager, cache_enabled=is_enabled + kernel_spec_manager=kernel_spec_manager, + cache_enabled=is_enabled, + parent=kernel_spec_manager.parent, ) yield kspec_cache - kspec_cache = None - KernelSpecCache.clear_instance() + kspec_cache.clear_instance() @pytest.fixture(params=[False, True]) # Add types as needed @@ -134,11 +146,14 @@ async def tests_get_named_spec(kernel_spec_cache): async def tests_get_modified_spec(kernel_spec_cache): kspec = await kernel_spec_cache.get_kernel_spec("test2") assert kspec.display_name == "Test kernel: test2" + assert set(kspec.metadata['client_envs']) == set(UNION_CLIENT_ENVS) # Modify entry _modify_kernelspec(kspec.resource_dir, "test2") + await asyncio.sleep(0.5) # sleep for a half-second to allow cache to update item kspec = await kernel_spec_cache.get_kernel_spec("test2") assert kspec.display_name == "test2 modified!" + assert set(kspec.metadata['client_envs']) == set(UNION_CLIENT_ENVS) async def tests_add_spec(kernel_spec_cache, kernelspec_location, other_kernelspec_location): @@ -180,6 +195,7 @@ async def tests_remove_spec(kernel_spec_cache): assert kernel_spec_cache.cache_misses == 0 shutil.rmtree(kspec.resource_dir) + await asyncio.sleep(0.5) # sleep for a half-second to allow cache to remove item with pytest.raises(NoSuchKernel): await kernel_spec_cache.get_kernel_spec("test2") From 5a3092fea2d159277b6be3a38128f272983cb180 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Wed, 27 Jul 2022 15:44:56 -0700 Subject: [PATCH 4/4] Revert changes to drop wildcard support and send client_envs in kspec --- .../services/kernels/handlers.py | 12 ++- .../services/kernels/remotemanager.py | 2 +- .../services/kernelspecs/kernelspec_cache.py | 32 +------- enterprise_gateway/tests/test_handlers.py | 82 +++++++++---------- .../tests/test_kernelspec_cache.py | 22 +---- 5 files changed, 58 insertions(+), 92 deletions(-) diff --git a/enterprise_gateway/services/kernels/handlers.py b/enterprise_gateway/services/kernels/handlers.py index f3ffe0b0d..7f6c1e8a3 100644 --- a/enterprise_gateway/services/kernels/handlers.py +++ b/enterprise_gateway/services/kernels/handlers.py @@ -54,16 +54,24 @@ async def post(self): # 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 + # Transfer inherited environment variables from current process env.update( {key: value for key, value in os.environ.items() if key in self.inherited_envs} ) + # Allow all KERNEL_* envs and those specified in client_envs and set from client. If this EG + # instance is configured to accept all envs in the payload (i.e., client_envs == '*'), go ahead + # and add those keys to the "working" allowed_envs list, otherwise, just transfer the configured envs. + allowed_envs: List[str] + if self.client_envs == ["*"]: + allowed_envs = model["env"].keys() + else: + allowed_envs = self.client_envs # Allow KERNEL_* args and those allowed by configuration. env.update( { key: value for key, value in model["env"].items() - if key.startswith("KERNEL_") or key in self.client_envs + if key.startswith("KERNEL_") or key in allowed_envs } ) diff --git a/enterprise_gateway/services/kernels/remotemanager.py b/enterprise_gateway/services/kernels/remotemanager.py index 8f68fff96..5276fc1b4 100644 --- a/enterprise_gateway/services/kernels/remotemanager.py +++ b/enterprise_gateway/services/kernels/remotemanager.py @@ -456,7 +456,7 @@ async def start_kernel(self, **kwargs): def _capture_user_overrides(self, **kwargs): """ - Make a copy of any whitelist or KERNEL_ env values provided by user. These will be injected + Make a copy of any allowed or KERNEL_ env values provided by user. These will be injected back into the env after the kernelspec env has been applied. This enables defaulting behavior of the kernelspec env stanza that would have otherwise overridden the user-provided values. """ diff --git a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py index 7ccb09c79..ed272b5b7 100644 --- a/enterprise_gateway/services/kernelspecs/kernelspec_cache.py +++ b/enterprise_gateway/services/kernelspecs/kernelspec_cache.py @@ -3,7 +3,7 @@ """Cache handling for kernel specs.""" import os -from typing import Dict, Optional, Set, Union +from typing import Dict, Optional, Union from jupyter_client.kernelspec import KernelSpec from jupyter_server.utils import ensure_async @@ -135,36 +135,8 @@ def put_item(self, kernel_name: str, cache_item: Union[KernelSpec, CacheItemType If it determines the cache entry corresponds to a currently unwatched directory, that directory will be added to list of observed directories and scheduled accordingly. """ - self.log.info(f"KernelSpecCache: adding/updating kernelspec: {kernel_name}") - # Irrespective of cache enablement, add/update the 'metadata.client_envs' entry - # with the set of configured values. If the stanza already exists in the kernelspec - # update with the union of it and those values configured via `EnterpriseGatewayApp'. - # We apply this logic here so that its only performed once for cached values or on - # every retrieval when caching is not enabled. - # Note: We only need to do this if we have a KernelSpec instance, since CacheItemType - # instances will have already been modified. - - # Create a set from the configured value, update it with the (potential) value - # in the kernelspec, and apply the changes back to the kernelspec. - - client_envs: Set[str] = set(self.parent.client_envs) - kspec_client_envs: Set[str] - if type(cache_item) is KernelSpec: - kspec: KernelSpec = cache_item - kspec_client_envs = set(kspec.metadata.get("client_envs", [])) - else: - kspec_client_envs = set(cache_item["spec"].get("metadata", {}).get("client_envs", [])) - - client_envs.update(kspec_client_envs) - if type(cache_item) is KernelSpec: - kspec: KernelSpec = cache_item - kspec.metadata["client_envs"] = list(client_envs) - else: - if "metadata" not in cache_item["spec"]: - cache_item["spec"]["metadata"] = {} - cache_item["spec"]["metadata"]["client_envs"] = list(client_envs) - if self.cache_enabled: + self.log.info(f"KernelSpecCache: adding/updating kernelspec: {kernel_name}") if type(cache_item) is KernelSpec: cache_item = KernelSpecCache.kernel_spec_to_cache_item(cache_item) diff --git a/enterprise_gateway/tests/test_handlers.py b/enterprise_gateway/tests/test_handlers.py index 0302f5354..be89d7484 100644 --- a/enterprise_gateway/tests/test_handlers.py +++ b/enterprise_gateway/tests/test_handlers.py @@ -644,44 +644,44 @@ def test_base_url(self): self.assertEqual(response.code, 200) -# class TestWildcardEnvs(TestHandlers): -# """Base class for jupyter-websocket mode tests that spawn kernels.""" -# -# def setup_app(self): -# """Configure JUPYTER_PATH so that we can use local kernelspec files for testing.""" -# super().setup_app() -# # overwrite env_whitelist -# self.app.env_whitelist = ["*"] -# -# @gen_test -# def test_kernel_wildcard_env(self): -# """Kernel should start with environment vars defined in the request.""" -# # Note: Since env_whitelist == '*', all values should be present. -# kernel_body = json.dumps( -# { -# "name": "python", -# "env": { -# "KERNEL_FOO": "kernel-foo-value", -# "OTHER_VAR1": "other-var1-value", -# "OTHER_VAR2": "other-var2-value", -# "TEST_VAR": "test-var-value", -# }, -# } -# ) -# ws = yield self.spawn_kernel(kernel_body) -# req = self.execute_request( -# "import os; " -# 'print(os.getenv("KERNEL_FOO"), ' -# 'os.getenv("OTHER_VAR1"), ' -# 'os.getenv("OTHER_VAR2"), ' -# 'os.getenv("TEST_VAR"))' -# ) -# ws.write_message(json_encode(req)) -# content = yield self.await_stream(ws) -# self.assertEqual(content["name"], "stdout") -# self.assertIn("kernel-foo-value", content["text"]) -# self.assertIn("other-var1-value", content["text"]) -# self.assertIn("other-var2-value", content["text"]) -# self.assertIn("test-var-value", content["text"]) -# -# ws.close() +class TestWildcardEnvs(TestHandlers): + """Base class for jupyter-websocket mode tests that spawn kernels.""" + + def setup_app(self): + """Configure JUPYTER_PATH so that we can use local kernelspec files for testing.""" + super().setup_app() + # overwrite env_whitelist + self.app.env_whitelist = ["*"] + + @gen_test + def test_kernel_wildcard_env(self): + """Kernel should start with environment vars defined in the request.""" + # Note: Since env_whitelist == '*', all values should be present. + kernel_body = json.dumps( + { + "name": "python", + "env": { + "KERNEL_FOO": "kernel-foo-value", + "OTHER_VAR1": "other-var1-value", + "OTHER_VAR2": "other-var2-value", + "TEST_VAR": "test-var-value", + }, + } + ) + ws = yield self.spawn_kernel(kernel_body) + req = self.execute_request( + "import os; " + 'print(os.getenv("KERNEL_FOO"), ' + 'os.getenv("OTHER_VAR1"), ' + 'os.getenv("OTHER_VAR2"), ' + 'os.getenv("TEST_VAR"))' + ) + ws.write_message(json_encode(req)) + content = yield self.await_stream(ws) + self.assertEqual(content["name"], "stdout") + self.assertIn("kernel-foo-value", content["text"]) + self.assertIn("other-var1-value", content["text"]) + self.assertIn("other-var2-value", content["text"]) + self.assertIn("test-var-value", content["text"]) + + ws.close() diff --git a/enterprise_gateway/tests/test_kernelspec_cache.py b/enterprise_gateway/tests/test_kernelspec_cache.py index e27be1a61..df18b8de2 100644 --- a/enterprise_gateway/tests/test_kernelspec_cache.py +++ b/enterprise_gateway/tests/test_kernelspec_cache.py @@ -12,7 +12,6 @@ import pytest from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel -from enterprise_gateway.enterprisegatewayapp import EnterpriseGatewayApp from enterprise_gateway.services.kernelspecs import KernelSpecCache @@ -61,16 +60,10 @@ def environ( # END - Remove once transition to jupyter_server occurs -GLOBAL_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] -KSPEC_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR3", "OTHER_VAR4"] -UNION_CLIENT_ENVS = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2", "OTHER_VAR3", "OTHER_VAR4"] kernelspec_json = { "argv": ["cat", "{connection_file}"], "display_name": "Test kernel: {kernel_name}", - "metadata": { - "client_envs": KSPEC_CLIENT_ENVS, - } } @@ -110,22 +103,17 @@ def setup_kernelspecs(environ, kernelspec_location): @pytest.fixture def kernel_spec_manager(environ, setup_kernelspecs): - app = EnterpriseGatewayApp() - app.client_envs = ["TEST_VAR", "OTHER_VAR1", "OTHER_VAR2"] - - yield KernelSpecManager(ensure_native_kernel=False, parent=app) + yield KernelSpecManager(ensure_native_kernel=False) @pytest.fixture def kernel_spec_cache(is_enabled, kernel_spec_manager): - kspec_cache = KernelSpecCache.instance( - kernel_spec_manager=kernel_spec_manager, - cache_enabled=is_enabled, - parent=kernel_spec_manager.parent, + kernel_spec_manager=kernel_spec_manager, cache_enabled=is_enabled ) yield kspec_cache - kspec_cache.clear_instance() + kspec_cache = None + KernelSpecCache.clear_instance() @pytest.fixture(params=[False, True]) # Add types as needed @@ -146,14 +134,12 @@ async def tests_get_named_spec(kernel_spec_cache): async def tests_get_modified_spec(kernel_spec_cache): kspec = await kernel_spec_cache.get_kernel_spec("test2") assert kspec.display_name == "Test kernel: test2" - assert set(kspec.metadata['client_envs']) == set(UNION_CLIENT_ENVS) # Modify entry _modify_kernelspec(kspec.resource_dir, "test2") await asyncio.sleep(0.5) # sleep for a half-second to allow cache to update item kspec = await kernel_spec_cache.get_kernel_spec("test2") assert kspec.display_name == "test2 modified!" - assert set(kspec.metadata['client_envs']) == set(UNION_CLIENT_ENVS) async def tests_add_spec(kernel_spec_cache, kernelspec_location, other_kernelspec_location):