Skip to content

Commit

Permalink
Merge pull request #92 from Peefy/init-strategic-merge-patch-module
Browse files Browse the repository at this point in the history
feat: impl k8s strategic merge patch
  • Loading branch information
Peefy authored Dec 5, 2023
2 parents 51138d8 + e95280f commit 01b9ca9
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 70 deletions.
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}

0 comments on commit 01b9ca9

Please sign in to comment.