From e95280fc549421b58bde3be1a0fd2af6f18b61d4 Mon Sep 17 00:00:00 2001 From: peefy Date: Tue, 5 Dec 2023 11:44:25 +0800 Subject: [PATCH] feat: impl k8s strategic merge patch Signed-off-by: peefy --- looper/README.md | 21 +++++ looper/kcl.mod | 2 +- looper/main.k | 12 +++ strategic_merge_patch/README.md | 101 +++++++++++++--------- strategic_merge_patch/kcl.mod | 2 +- strategic_merge_patch/main.k | 67 ++++++++------ strategic_merge_patch/main_test.k | 65 ++++++++++++++ strategic_merge_patch/strategy/strategy.k | 6 ++ 8 files changed, 206 insertions(+), 70 deletions(-) create mode 100644 strategic_merge_patch/main_test.k diff --git a/looper/README.md b/looper/README.md index 98e93ce7..ba1da683 100644 --- a/looper/README.md +++ b/looper/README.md @@ -2,6 +2,27 @@ `looper` is a KCL loop library +## How to Use + ++ Add the dependency + +```shell +kcl mod add looper +``` + ++ Write the code + +```python +import looper + +result1 = looper(0, [1, 2, 3], lambda i, v { + i + v +}) # 6 +result2 = looper(1, [2, 2, 2], lambda i, v { + i * v +}) # 8 +``` + ## Resource The Code source and documents are [here](https://github.com/kcl-lang/modules/tree/main/looper) diff --git a/looper/kcl.mod b/looper/kcl.mod index d66e5497..46453bb0 100644 --- a/looper/kcl.mod +++ b/looper/kcl.mod @@ -1,5 +1,5 @@ [package] name = "looper" -version = "0.0.1" +version = "0.1.0" description = "`looper` is a KCL loop library" diff --git a/looper/main.k b/looper/main.k index 744da526..535cd56f 100644 --- a/looper/main.k +++ b/looper/main.k @@ -10,6 +10,18 @@ looper = lambda initial: any, elements: [any], func: (any, any) -> any -> any { _looper_n(elements, 0, func, initial) } +_looper_n_with_param = lambda elements: [any], n: int, func: (any, any, any) -> any, initial: any, param: any -> any { + assert n >= 0 + result = initial + if n < len(elements): + result = _looper_n_with_param(elements, n + 1, func, func(result, elements[n], param)) + result +} + +looper_with_param = lambda initial: any, elements: [any], func: (any, any, any) -> any, param: any -> any { + _looper_n_with_param(elements, 0, func, initial, param) +} + for_each = lambda elements: [any], func: (any) -> any { [func(i) for i in elements] Undefined diff --git a/strategic_merge_patch/README.md b/strategic_merge_patch/README.md index be0c4a9e..bc4818db 100644 --- a/strategic_merge_patch/README.md +++ b/strategic_merge_patch/README.md @@ -3,7 +3,8 @@ `strategic_merge_patch` is a module for applying Kubernetes strategic merge patches for KCL values. Notice this library is WIP. + [x] Kubernetes Extension Merge Strategic Definition. -+ [ ] `strategic_merge_patch.merge` function impl. ++ [x] `strategic_merge_patch.merge` function impl. ++ [ ] Directives: `$retainKeys`, "$patch", `$deleteFromPrimitiveList/: [a primitive list]`, `# $setElementOrder/: [a list]`, etc. ## How to Use @@ -16,47 +17,67 @@ kcl mod add strategic_merge_patch + Write the code ```python -import strategic_merge_patch as p - -data1 = { - "firstName": "John", - "lastName": "Doe", - "age": 30, - "address": { - "streetAddress": "1234 Main St", - "city": "New York", - "state": "NY", - "postalCode": "10001" - }, - "phoneNumbers": [ - { - "type": "home", - "number": "212-555-1234" - }, - { - "type": "work", - "number": "646-555-5678" + original = { + "metadata": { + "name": "my-deployment" + "labels": {"app": "my-app"} + } + "spec": { + "replicas": 3 + "template": { + "spec": {"containers": [ + { + "name" = "my-container-1" + "image" = "my-image-1" + } + { + "name" = "my-container-2" + "image" = "my-image-2" + } + ]} + } + } + } + patch = { + "metadata": { + "labels": {"version": "v1"} } - ] -} -data2 = { - "firstName": "John", - "lastName": "Doe", - "age": 30, - "address": { - "streetAddress": "1234 Main St", - "city": "New York", - "state": "NY", - "postalCode": None - }, - "phoneNumbers": [ - { - "type": "work", - "number": "646-555-5678" + "spec": { + "replicas": 4 + "template": { + "spec": {"containers": [ + { + "name" = "my-container-1" + "image" = "my-new-image-1" + } + { + "name": "my-container-3" + "image" = "my-image-3" + } + ]} + } } - ] -} -data_merge = p.merge(data1, data2) + } + expected = yaml.decode("""\ +metadata: + name: my-deployment + labels: + app: my-app + version: v1 +spec: + replicas: 4 + template: + spec: + containers: + - name: my-container-1 + image: my-new-image-1 + - name: my-container-2 + image: my-image-2 + - name: my-container-3 + image: my-image-3 +""") + got = merge(original, patch) + assert str(got) == str(expected), "expected ${expected}, got ${got}" ``` ## Resource diff --git a/strategic_merge_patch/kcl.mod b/strategic_merge_patch/kcl.mod index 77dc30e8..3855b8d5 100644 --- a/strategic_merge_patch/kcl.mod +++ b/strategic_merge_patch/kcl.mod @@ -1,5 +1,5 @@ [package] name = "strategic_merge_patch" -version = "0.0.1" +version = "0.1.0" description = "`strategic_merge_patch` is a module for applying Kubernetes strategic merge patches for KCL values." diff --git a/strategic_merge_patch/main.k b/strategic_merge_patch/main.k index c8be6ebf..af46b811 100644 --- a/strategic_merge_patch/main.k +++ b/strategic_merge_patch/main.k @@ -1,9 +1,10 @@ +import strategy + KCL_BUILTIN_TYPES = ["int", "str", "bool", "float", "None", "UndefinedType", "any", "list", "dict", "function", "number_multiplier"] NULL_CONSTANTS = [Undefined, None] is_schema = lambda obj: any -> bool { typeof(obj) not in KCL_BUILTIN_TYPES - } is_config = lambda obj: any -> bool { @@ -14,7 +15,7 @@ is_list = lambda obj: any -> bool { typeof(obj) == "list" } -_looper_n = lambda elements: [any], n: int, func: (any, any) -> any, initial: any-> any { +_looper_n = lambda elements: [any], n: int, func: (any, any) -> any, initial: any -> any { assert n >= 0 result = initial if n < len(elements): @@ -26,10 +27,16 @@ looper = lambda initial: any, elements: [any], func: (any, any) -> any -> any { _looper_n(elements, 0, func, initial) } -for_each = lambda elements: [any], func: (any) -> any { - _looper_n(elements, 0, lambda v, e { - func(e) - }, Undefined) +_looper_n_with_param = lambda elements: [any], n: int, func: (any, any, any) -> any, initial: any, param: any -> any { + assert n >= 0 + result = initial + if n < len(elements): + result = _looper_n_with_param(elements, n + 1, func, func(result, elements[n], param)) + result +} + +looper_with_param = lambda initial: any, elements: [any], func: (any, any, any) -> any, param: any -> any { + _looper_n_with_param(elements, 0, func, initial, param) } looper_enumerate = lambda initial: any, elements: [any] | {str:}, func: (any, str | int, any) -> any -> any { @@ -38,26 +45,30 @@ looper_enumerate = lambda initial: any, elements: [any] | {str:}, func: (any, st }) } -merge = lambda src: any, obj: any -> any { - result = src - if not is_config(src): - result = {} - if not is_config(obj): - result = obj - else: - result = looper_enumerate(result, obj, lambda result, key, value { - target = result[key] - if is_config(value): - if is_config(target): - result |= {"{}".format(key) = merge(target, value)} - else: - result |= {"{}".format(key) = merge({}, value)} - elif value in NULL_CONSTANTS: - result |= {"{}".format(key) = Undefined} - result = {k: v for k, v in result if k != key} - else: - result |= {"{}".format(key) = value} - result - }) - result +merge = lambda org: any, patch: any -> any { + looper_enumerate(org, patch, lambda result, key, value { + target = result[key] + if key in result and is_config(value) and is_config(result[key]): + result |= {"{}".format(key) = merge(result[key], value)} + elif key in result and is_list(value) and is_list(result[key]): + result |= {"{}".format(key) = merge_list_with_property(result[key], value, key)} + elif value in NULL_CONSTANTS: + result |= {"{}".format(key) = Undefined} + result = {k: v for k, v in result if k != key} + else: + result |= {"{}".format(key) = value} + result + }) +} + +merge_list_with_property = lambda org: [any], patch: [any], name: str = Undefined -> [any] { + key: str = strategy.PATCH_MERGE_KEYS[name] if name and name in strategy.PATCH_MERGE_KEYS else Undefined + result = looper_with_param(org, patch, lambda result, item, key { + existing_item_list = [i for i, x in result if key in x and x[key] == item[key]] + if existing_item_list: + result |= [item if key in x and x[key] == item[key] else {} for x in result] + else: + result += [item] + result + }, key) if key else patch } diff --git a/strategic_merge_patch/main_test.k b/strategic_merge_patch/main_test.k new file mode 100644 index 00000000..0c518e3f --- /dev/null +++ b/strategic_merge_patch/main_test.k @@ -0,0 +1,65 @@ +import yaml + +test_merge = lambda { + original = { + "metadata": { + "name": "my-deployment" + "labels": {"app": "my-app"} + } + "spec": { + "replicas": 3 + "template": { + "spec": {"containers": [ + { + "name": "my-container-1" + "image": "my-image-1" + } + { + "name": "my-container-2" + "image": "my-image-2" + } + ]} + } + } + } + patch = { + "metadata": { + "labels": {"version": "v1"} + } + "spec": { + "replicas": 4 + "template": { + "spec": {"containers": [ + { + "name": "my-container-1" + "image" = "my-new-image-1" + } + { + "name": "my-container-3" + "image" = "my-image-3" + } + ]} + } + } + } + expected = yaml.decode("""\ +metadata: + name: my-deployment + labels: + app: my-app + version: v1 +spec: + replicas: 4 + template: + spec: + containers: + - name: my-container-1 + image: my-new-image-1 + - name: my-container-2 + image: my-image-2 + - name: my-container-3 + image: my-image-3 +""") + got = merge(original, patch) + assert str(got) == str(expected), "expected ${expected}, got ${got}" +} diff --git a/strategic_merge_patch/strategy/strategy.k b/strategic_merge_patch/strategy/strategy.k index 144b0373..310f0d6d 100644 --- a/strategic_merge_patch/strategy/strategy.k +++ b/strategic_merge_patch/strategy/strategy.k @@ -1,3 +1,5 @@ +import strategy.k8s + X_PATCH_STRATEGY = "x-kubernetes-patch-strategy" X_PATCH_MERGE_KEY = "x-kubernetes-patch-merge-key" X_LIST_MAP_KEYS = "x-kubernetes-list-map-keys" @@ -13,3 +15,7 @@ SET_ELEMENT_ORDER_DIRECTIV = "$setElementOrder" REPLACE_ACTION = "replace" DELETE_ACTION = "delete" MERGE_ACTION = "merge" + +# Notice there are no CRD definitions here. + +PATCH_MERGE_KEYS = {"{}".format(key) = p[X_PATCH_MERGE_KEY] for _, d in k8s.definitions for key, p in d.properties if X_PATCH_MERGE_KEY in p}