Skip to content

Commit e95280f

Browse files
committed
feat: impl k8s strategic merge patch
Signed-off-by: peefy <[email protected]>
1 parent 51138d8 commit e95280f

File tree

8 files changed

+206
-70
lines changed

8 files changed

+206
-70
lines changed

looper/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
`looper` is a KCL loop library
44

5+
## How to Use
6+
7+
+ Add the dependency
8+
9+
```shell
10+
kcl mod add looper
11+
```
12+
13+
+ Write the code
14+
15+
```python
16+
import looper
17+
18+
result1 = looper(0, [1, 2, 3], lambda i, v {
19+
i + v
20+
}) # 6
21+
result2 = looper(1, [2, 2, 2], lambda i, v {
22+
i * v
23+
}) # 8
24+
```
25+
526
## Resource
627

728
The Code source and documents are [here](https://github.com/kcl-lang/modules/tree/main/looper)

looper/kcl.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
22
name = "looper"
3-
version = "0.0.1"
3+
version = "0.1.0"
44
description = "`looper` is a KCL loop library"
55

looper/main.k

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ looper = lambda initial: any, elements: [any], func: (any, any) -> any -> any {
1010
_looper_n(elements, 0, func, initial)
1111
}
1212

13+
_looper_n_with_param = lambda elements: [any], n: int, func: (any, any, any) -> any, initial: any, param: any -> any {
14+
assert n >= 0
15+
result = initial
16+
if n < len(elements):
17+
result = _looper_n_with_param(elements, n + 1, func, func(result, elements[n], param))
18+
result
19+
}
20+
21+
looper_with_param = lambda initial: any, elements: [any], func: (any, any, any) -> any, param: any -> any {
22+
_looper_n_with_param(elements, 0, func, initial, param)
23+
}
24+
1325
for_each = lambda elements: [any], func: (any) -> any {
1426
[func(i) for i in elements]
1527
Undefined

strategic_merge_patch/README.md

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
`strategic_merge_patch` is a module for applying Kubernetes strategic merge patches for KCL values. Notice this library is WIP.
44

55
+ [x] Kubernetes Extension Merge Strategic Definition.
6-
+ [ ] `strategic_merge_patch.merge` function impl.
6+
+ [x] `strategic_merge_patch.merge` function impl.
7+
+ [ ] Directives: `$retainKeys`, "$patch", `$deleteFromPrimitiveList/<keyOfPrimitiveList>: [a primitive list]`, `# $setElementOrder/<keyOfList>: [a list]`, etc.
78

89
## How to Use
910

@@ -16,47 +17,67 @@ kcl mod add strategic_merge_patch
1617
+ Write the code
1718

1819
```python
19-
import strategic_merge_patch as p
20-
21-
data1 = {
22-
"firstName": "John",
23-
"lastName": "Doe",
24-
"age": 30,
25-
"address": {
26-
"streetAddress": "1234 Main St",
27-
"city": "New York",
28-
"state": "NY",
29-
"postalCode": "10001"
30-
},
31-
"phoneNumbers": [
32-
{
33-
"type": "home",
34-
"number": "212-555-1234"
35-
},
36-
{
37-
"type": "work",
38-
"number": "646-555-5678"
20+
original = {
21+
"metadata": {
22+
"name": "my-deployment"
23+
"labels": {"app": "my-app"}
24+
}
25+
"spec": {
26+
"replicas": 3
27+
"template": {
28+
"spec": {"containers": [
29+
{
30+
"name" = "my-container-1"
31+
"image" = "my-image-1"
32+
}
33+
{
34+
"name" = "my-container-2"
35+
"image" = "my-image-2"
36+
}
37+
]}
38+
}
39+
}
40+
}
41+
patch = {
42+
"metadata": {
43+
"labels": {"version": "v1"}
3944
}
40-
]
41-
}
42-
data2 = {
43-
"firstName": "John",
44-
"lastName": "Doe",
45-
"age": 30,
46-
"address": {
47-
"streetAddress": "1234 Main St",
48-
"city": "New York",
49-
"state": "NY",
50-
"postalCode": None
51-
},
52-
"phoneNumbers": [
53-
{
54-
"type": "work",
55-
"number": "646-555-5678"
45+
"spec": {
46+
"replicas": 4
47+
"template": {
48+
"spec": {"containers": [
49+
{
50+
"name" = "my-container-1"
51+
"image" = "my-new-image-1"
52+
}
53+
{
54+
"name": "my-container-3"
55+
"image" = "my-image-3"
56+
}
57+
]}
58+
}
5659
}
57-
]
58-
}
59-
data_merge = p.merge(data1, data2)
60+
}
61+
expected = yaml.decode("""\
62+
metadata:
63+
name: my-deployment
64+
labels:
65+
app: my-app
66+
version: v1
67+
spec:
68+
replicas: 4
69+
template:
70+
spec:
71+
containers:
72+
- name: my-container-1
73+
image: my-new-image-1
74+
- name: my-container-2
75+
image: my-image-2
76+
- name: my-container-3
77+
image: my-image-3
78+
""")
79+
got = merge(original, patch)
80+
assert str(got) == str(expected), "expected ${expected}, got ${got}"
6081
```
6182

6283
## Resource

strategic_merge_patch/kcl.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
22
name = "strategic_merge_patch"
3-
version = "0.0.1"
3+
version = "0.1.0"
44
description = "`strategic_merge_patch` is a module for applying Kubernetes strategic merge patches for KCL values."
55

strategic_merge_patch/main.k

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import strategy
2+
13
KCL_BUILTIN_TYPES = ["int", "str", "bool", "float", "None", "UndefinedType", "any", "list", "dict", "function", "number_multiplier"]
24
NULL_CONSTANTS = [Undefined, None]
35

46
is_schema = lambda obj: any -> bool {
57
typeof(obj) not in KCL_BUILTIN_TYPES
6-
78
}
89

910
is_config = lambda obj: any -> bool {
@@ -14,7 +15,7 @@ is_list = lambda obj: any -> bool {
1415
typeof(obj) == "list"
1516
}
1617

17-
_looper_n = lambda elements: [any], n: int, func: (any, any) -> any, initial: any-> any {
18+
_looper_n = lambda elements: [any], n: int, func: (any, any) -> any, initial: any -> any {
1819
assert n >= 0
1920
result = initial
2021
if n < len(elements):
@@ -26,10 +27,16 @@ looper = lambda initial: any, elements: [any], func: (any, any) -> any -> any {
2627
_looper_n(elements, 0, func, initial)
2728
}
2829

29-
for_each = lambda elements: [any], func: (any) -> any {
30-
_looper_n(elements, 0, lambda v, e {
31-
func(e)
32-
}, Undefined)
30+
_looper_n_with_param = lambda elements: [any], n: int, func: (any, any, any) -> any, initial: any, param: any -> any {
31+
assert n >= 0
32+
result = initial
33+
if n < len(elements):
34+
result = _looper_n_with_param(elements, n + 1, func, func(result, elements[n], param))
35+
result
36+
}
37+
38+
looper_with_param = lambda initial: any, elements: [any], func: (any, any, any) -> any, param: any -> any {
39+
_looper_n_with_param(elements, 0, func, initial, param)
3340
}
3441

3542
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
3845
})
3946
}
4047

41-
merge = lambda src: any, obj: any -> any {
42-
result = src
43-
if not is_config(src):
44-
result = {}
45-
if not is_config(obj):
46-
result = obj
47-
else:
48-
result = looper_enumerate(result, obj, lambda result, key, value {
49-
target = result[key]
50-
if is_config(value):
51-
if is_config(target):
52-
result |= {"{}".format(key) = merge(target, value)}
53-
else:
54-
result |= {"{}".format(key) = merge({}, value)}
55-
elif value in NULL_CONSTANTS:
56-
result |= {"{}".format(key) = Undefined}
57-
result = {k: v for k, v in result if k != key}
58-
else:
59-
result |= {"{}".format(key) = value}
60-
result
61-
})
62-
result
48+
merge = lambda org: any, patch: any -> any {
49+
looper_enumerate(org, patch, lambda result, key, value {
50+
target = result[key]
51+
if key in result and is_config(value) and is_config(result[key]):
52+
result |= {"{}".format(key) = merge(result[key], value)}
53+
elif key in result and is_list(value) and is_list(result[key]):
54+
result |= {"{}".format(key) = merge_list_with_property(result[key], value, key)}
55+
elif value in NULL_CONSTANTS:
56+
result |= {"{}".format(key) = Undefined}
57+
result = {k: v for k, v in result if k != key}
58+
else:
59+
result |= {"{}".format(key) = value}
60+
result
61+
})
62+
}
63+
64+
merge_list_with_property = lambda org: [any], patch: [any], name: str = Undefined -> [any] {
65+
key: str = strategy.PATCH_MERGE_KEYS[name] if name and name in strategy.PATCH_MERGE_KEYS else Undefined
66+
result = looper_with_param(org, patch, lambda result, item, key {
67+
existing_item_list = [i for i, x in result if key in x and x[key] == item[key]]
68+
if existing_item_list:
69+
result |= [item if key in x and x[key] == item[key] else {} for x in result]
70+
else:
71+
result += [item]
72+
result
73+
}, key) if key else patch
6374
}

strategic_merge_patch/main_test.k

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import yaml
2+
3+
test_merge = lambda {
4+
original = {
5+
"metadata": {
6+
"name": "my-deployment"
7+
"labels": {"app": "my-app"}
8+
}
9+
"spec": {
10+
"replicas": 3
11+
"template": {
12+
"spec": {"containers": [
13+
{
14+
"name": "my-container-1"
15+
"image": "my-image-1"
16+
}
17+
{
18+
"name": "my-container-2"
19+
"image": "my-image-2"
20+
}
21+
]}
22+
}
23+
}
24+
}
25+
patch = {
26+
"metadata": {
27+
"labels": {"version": "v1"}
28+
}
29+
"spec": {
30+
"replicas": 4
31+
"template": {
32+
"spec": {"containers": [
33+
{
34+
"name": "my-container-1"
35+
"image" = "my-new-image-1"
36+
}
37+
{
38+
"name": "my-container-3"
39+
"image" = "my-image-3"
40+
}
41+
]}
42+
}
43+
}
44+
}
45+
expected = yaml.decode("""\
46+
metadata:
47+
name: my-deployment
48+
labels:
49+
app: my-app
50+
version: v1
51+
spec:
52+
replicas: 4
53+
template:
54+
spec:
55+
containers:
56+
- name: my-container-1
57+
image: my-new-image-1
58+
- name: my-container-2
59+
image: my-image-2
60+
- name: my-container-3
61+
image: my-image-3
62+
""")
63+
got = merge(original, patch)
64+
assert str(got) == str(expected), "expected ${expected}, got ${got}"
65+
}

strategic_merge_patch/strategy/strategy.k

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import strategy.k8s
2+
13
X_PATCH_STRATEGY = "x-kubernetes-patch-strategy"
24
X_PATCH_MERGE_KEY = "x-kubernetes-patch-merge-key"
35
X_LIST_MAP_KEYS = "x-kubernetes-list-map-keys"
@@ -13,3 +15,7 @@ SET_ELEMENT_ORDER_DIRECTIV = "$setElementOrder"
1315
REPLACE_ACTION = "replace"
1416
DELETE_ACTION = "delete"
1517
MERGE_ACTION = "merge"
18+
19+
# Notice there are no CRD definitions here.
20+
21+
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}

0 commit comments

Comments
 (0)