Skip to content

Commit

Permalink
Support lists & dicts in unlisted_choice kubespawner_override
Browse files Browse the repository at this point in the history
  • Loading branch information
yuvipanda committed Sep 14, 2023
1 parent eb08583 commit 83dea29
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 6 deletions.
7 changes: 2 additions & 5 deletions kubespawner/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
make_service,
)
from .reflector import ResourceReflector
from .utils import recursive_update
from .utils import recursive_format, recursive_update


class PodReflector(ResourceReflector):
Expand Down Expand Up @@ -3191,10 +3191,7 @@ def _load_profile(self, slug, profile_list):
"kubespawner_override", {}
)
for k, v in option_overrides.items():
# FIXME: This logic restricts unlisted_choice to define
# kubespawner_override dictionaries where all keys
# have string values.
option_overrides[k] = v.format(value=unlisted_choice)
option_overrides[k] = recursive_format(v, value=unlisted_choice)
elif choice:
# A pre-defined choice was selected
option_overrides = option["choices"][choice].get(
Expand Down
43 changes: 43 additions & 0 deletions kubespawner/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
import copy
import hashlib
from typing import Union


def generate_hashed_slug(slug, limit=63, hash_length=6):
Expand Down Expand Up @@ -222,3 +223,45 @@ def recursive_update(target, new):

else:
target[k] = v


class IgnoreMissing(dict):
"""
Dictionary subclass for use with format_map
Renders missing values as "{key}", so format strings with
missing values just get rendered as is.
Stolen from https://docs.python.org/3/library/stdtypes.html#str.format_map
"""

def __missing__(self, key):
return f"{{{key}}}"


def recursive_format(
format_object: Union[str, dict, list], **kwargs
) -> Union[str, dict, list]:
"""
Recursively format given object with values provided as keyword arguments.
If the given object (string, list or dict) has items that do not have
placeholders for passed in kwargs, no formatting is performed.
recursive_format("{v}", v=5) -> Returns "5"
recrusive_format("{a}") -> Returns "{a}" rather than erroring, as is
the behavior of "format"
"""
if isinstance(format_object, str):
return format_object.format_map(IgnoreMissing(kwargs))
elif isinstance(format_object, list):
return [recursive_format(i, **kwargs) for i in format_object]
elif isinstance(format_object, dict):
return {
recursive_format(k, **kwargs): recursive_format(v, **kwargs)
for k, v in format_object.items()
}
else:
raise ValueError(
f"Object of type {type(format_object)} can not be recursively formatted"
)
16 changes: 15 additions & 1 deletion tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,15 @@ async def test_unlisted_choice_non_string_override():
'environment': {
'CUSTOM_IMAGE_USED': 'yes',
'CUSTOM_IMAGE': '{value}',
# This should just be passed through, as JUPYTER_USER is not replaced
'USER': '${JUPYTER_USER}',
# This should render as ${JUPYTER_USER}, as the {{ and }} escape them.
# this matches existing behavior for other replacements elsewhere
'USER_TEST': '${{JUPYTER_USER}}',
},
"init_containers": [
{"name": "testing", "image": "{value}"}
],
},
},
}
Expand All @@ -274,7 +282,13 @@ async def test_unlisted_choice_non_string_override():
await spawner.load_user_options()

assert spawner.image == image
assert spawner.environment == {'CUSTOM_IMAGE_USED': 'yes', 'CUSTOM_IMAGE': image}
assert spawner.environment == {
'CUSTOM_IMAGE_USED': 'yes',
'CUSTOM_IMAGE': image,
'USER': '${JUPYTER_USER}',
'USER_TEST': '${JUPYTER_USER}',
}
assert spawner.init_containers == [{"name": "testing", "image": image}]


async def test_empty_user_options_and_profile_options_api():
Expand Down

0 comments on commit 83dea29

Please sign in to comment.