From 9a4744ba52d1358be064cdfbbee00207b6a69011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Pavl=C3=ADn?= Date: Tue, 11 Jun 2019 08:49:33 +0200 Subject: [PATCH] Improve profiles merging to make sure nested dicts are merged not replaced --- jupyterhub_singleuser_profiles/profiles.py | 58 +++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/jupyterhub_singleuser_profiles/profiles.py b/jupyterhub_singleuser_profiles/profiles.py index b80926c5..838581f2 100644 --- a/jupyterhub_singleuser_profiles/profiles.py +++ b/jupyterhub_singleuser_profiles/profiles.py @@ -57,11 +57,9 @@ def write_config_map(self, config_map_name, key_name, data): def update_user_profile_cm(self, username, data={}, key=None, value=None): cm_name = _USER_CONFIG_MAP_TEMPLATE % escape(username) cm_key_name = "profile" - cm_data = data - if len(data) > 0 and 'env' not in data: - cm_data = {'env': data} - if key and value: - cm_data[key] = value + cm_data = self.get_user_profile_cm(username) + cm_data = self.merge_profiles(cm_data, data) + self.write_config_map(cm_name, cm_key_name, cm_data) def get_user_profile_cm(self, username): @@ -171,13 +169,49 @@ def empty_profile(self): @classmethod def merge_profiles(self, profile1, profile2): - profile1["name"] = ", ".join(filter(None, [profile1.get("name", ""), profile2.get("name", "")])) - profile1["images"] = list(set(profile1.get("images", []) + profile2.get("images", []))) - profile1["users"] = list(set(profile1.get("users", []) + profile2.get("users", []))) - profile1["env"] = {**profile1.get('env', {}), **profile2.get('env', {})} - profile1["resources"] = {**profile1.get('resources', {}), **profile2.get('resources', {})} - profile1["services"] = {**profile1.get('services', {}), **profile2.get('services', {})} - return profile1 + name = ", ".join(filter(None, [profile1.get("name", ""), profile2.get("name", "")])) + result = {} + for k, v in profile1.items(): + result[k] = self._profile_data_merge(v, profile2.get(k, type(v)())) + + result["name"] = name + return result + + @classmethod + def _profile_data_merge(self, a, b): + """merges b into a and return merged result + + NOTE: tuples and arbitrary objects are not handled as it is totally ambiguous what should happen""" + key = None + # ## debug output + # sys.stderr.write("DEBUG: %s to %s\n" %(b,a)) + try: + if a is None or isinstance(a, str) or isinstance(a, int) or isinstance(a, float): + # border case for first run or if a is a primitive + a = b + elif isinstance(a, list): + # lists can be only appended + if isinstance(b, list): + # merge lists + a = list(set(a + b)) + else: + # append to list + a.append(b) + elif isinstance(a, dict): + # dicts must be merged + if isinstance(b, dict): + for key in b: + if key in a: + a[key] = self._profile_data_merge(a[key], b[key]) + else: + a[key] = b[key] + else: + raise Exception('Cannot merge non-dict "%s" into dict "%s"' % (b, a)) + else: + raise Exception('NOT IMPLEMENTED "%s" into "%s"' % (b, a)) + except TypeError as e: + raise Exception('TypeError "%s" in key "%s" when merging "%s" into "%s"' % (e, key, b, a)) + return a @classmethod def apply_pod_profile(self, spawner, pod, profile):