Skip to content

feat: impl k8s strategic merge patch #92

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

Merged
merged 1 commit into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions looper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion looper/kcl.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "looper"
version = "0.0.1"
version = "0.1.0"
description = "`looper` is a KCL loop library"

12 changes: 12 additions & 0 deletions looper/main.k
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
101 changes: 61 additions & 40 deletions strategic_merge_patch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<keyOfPrimitiveList>: [a primitive list]`, `# $setElementOrder/<keyOfList>: [a list]`, etc.

## How to Use

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion strategic_merge_patch/kcl.mod
Original file line number Diff line number Diff line change
@@ -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."

67 changes: 39 additions & 28 deletions strategic_merge_patch/main.k
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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):
Expand All @@ -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 {
Expand All @@ -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
}
65 changes: 65 additions & 0 deletions strategic_merge_patch/main_test.k
Original file line number Diff line number Diff line change
@@ -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}"
}
6 changes: 6 additions & 0 deletions strategic_merge_patch/strategy/strategy.k
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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}