Skip to content

Commit

Permalink
Add SelectItem and extend Go Template engine with Sprig Functions (#50)
Browse files Browse the repository at this point in the history
* Extend value with Sprig; Add .SelectedItem to template

* Prepare for release of v0.11.0

* Clarify SelectedItem example [skip ci]
  • Loading branch information
vassilvk authored Apr 16, 2021
1 parent cbf53ca commit e96f279
Show file tree
Hide file tree
Showing 17 changed files with 327 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.


## 0.11.0 - 2021-04-16

* 48: Extend Go Template engine with Sprig Functions

## 0.10.0 - 2021-01-30

* 45: Implement stability improvements for multi-node clusters
Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Run the following commands to deploy KubeMod.
# Make KubeMod ignore Kubernetes' system namespace.
kubectl label namespace kube-system admission.kubemod.io/ignore=true --overwrite
# Deploy KubeMod.
kubectl apply -f https://raw.githubusercontent.com/kubemod/kubemod/v0.10.0/bundle.yaml
kubectl apply -f https://raw.githubusercontent.com/kubemod/kubemod/v0.11.0/bundle.yaml
```

By default KubeMod allows you to target a limited set of high-level resource types, such as deployments and services.
Expand All @@ -60,15 +60,15 @@ kubectl delete job -l job-name -n kubemod-system
# Make KubeMod ignore Kubernetes' system namespace.
kubectl label namespace kube-system admission.kubemod.io/ignore=true --overwrite
# Upgrade KubeMod operator.
kubectl apply -f https://raw.githubusercontent.com/kubemod/kubemod/v0.10.0/bundle.yaml
kubectl apply -f https://raw.githubusercontent.com/kubemod/kubemod/v0.11.0/bundle.yaml
```

### Uninstall

To uninstall KubeMod and all its resources, run:

```bash
kubectl delete -f https://raw.githubusercontent.com/kubemod/kubemod/v0.10.0/bundle.yaml
kubectl delete -f https://raw.githubusercontent.com/kubemod/kubemod/v0.11.0/bundle.yaml
```

**Note**: Uninstalling KubeMod will also remove all your ModRules deployed to all Kubernetes namespaces.
Expand Down Expand Up @@ -712,11 +712,14 @@ value: |-

When `value` contains `{{ ... }}`, it is evaluated as a [Golang template](https://golang.org/pkg/text/template/).

In addition, the Golang template engine used by KubeMod is extended with the [Sprig library of template functions](http://masterminds.github.io/sprig/).

The following intrinsic items are accessible through the template's context:

* `.Target` — the original resource object being patched.
* `.Namespace` — the namespace of the target object.
* `.SelectKeyParts` - when `select` was used for the patch, `.SelectKeyParts` can be used in `value` to access
* `.SelectedItem` — when `select` was used for the patch, `.SelectedItem` yields the current result of the select evaluation. See second example below.
* `.SelectKeyParts` — when `select` was used for the patch, `.SelectKeyParts` can be used in `value` to access
the wildcard/filter values captured for this patch operation.

For example, the following excerpt of a Jaeger side-car injection `ModRule` includes a `value` which uses `{{ .Target.metadata.name }}` to access the name of the `Deployment` being patched.
Expand All @@ -738,6 +741,37 @@ value: |-

See full example of the above ModRule [here](#sidecar-injection).

#### Advanced use of SelectedItem

The presence of `.SelectedItem` in the `value` template unlocks some advanced scenarios.

For example, the following `patch` rule will match all containers from image repository `their-repo` and will replace the repository part of the image with `my-repo`,
keeping the rest of the image name intact:

```yaml
...
patch:
- op: replace
# Select only containers whose image belongs to container registry "their-repo".
select: '$.spec.containers[? @.image =~ "their-repo/.+"].image'
path: /spec/containers/#0/image
# Replace the existing value by running Sprig's regexReplaceAll function against .SelectedItem.
value: '{{ regexReplaceAll "(.+)/(.*)" .SelectedItem "my-repo/${2}" }}'
```

Note that `.SelectedItem` points to the part of the resource selected by the `select` expression.

In the above example, the `select` expression is `$.spec.containers[? @.image =~ "repo-1/.+"].image` so `.SelectedItem` is a string with the value of the image.

On the other hand, if the `select` expression was `$.spec.containers[? @.image =~ "repo-1/.+"]`, then `.SelectedItem` would be a string-to-value map with the properties
of the `container` object.

In that case, to access any of the properties of the container, one would use the `index` Golang template function.

For example:

`{{ index .SelectedItem "image" }}`

### `rejectMessage` \(string: optional\)

Field `rejectMessage` is an optional message displayed when a resource is rejected by a `Reject` ModRule.
Expand Down
5 changes: 2 additions & 3 deletions api/v1beta1/modrule_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package v1beta1

import (
"fmt"
"html/template"
"regexp"

"github.com/kubemod/kubemod/expressions"
Expand Down Expand Up @@ -137,7 +136,7 @@ func (r *ModRule) validateModRule() error {
value := *po.Value

// Test the template.
_, err := template.New(po.Path).Parse(util.PreProcessModRuleGoTemplate(value))
_, err := util.NewSafeTemplate(po.Path).Parse(util.PreProcessModRuleGoTemplate(value))

if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("patch").Index(i).Child("value"), value, fmt.Sprintf("%v", err)))
Expand All @@ -156,7 +155,7 @@ func (r *ModRule) validateModRule() error {

// Validate the rejectMessage as a template.
if r.Spec.RejectMessage != nil {
_, err = template.New("rejectMessage").Parse(*r.Spec.RejectMessage)
_, err = util.NewSafeTemplate("rejectMessage").Parse(*r.Spec.RejectMessage)

if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("rejectMessage"), *r.Spec.RejectMessage, fmt.Sprintf("%v", err)))
Expand Down
2 changes: 1 addition & 1 deletion bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ spec:
- /kubemod
- -operator
- -webapp
image: kubemod/kubemod:v0.10.0
image: kubemod/kubemod:v0.11.0
livenessProbe:
httpGet:
path: /healthz
Expand Down
2 changes: 1 addition & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ kind: Kustomization
images:
- name: controller
newName: kubemod/kubemod
newTag: v0.10.0
newTag: v0.11.0
2 changes: 2 additions & 0 deletions core/modrulestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ var _ = Describe("ModRuleStore", func() {
Entry("patch-10 on deployment-1 should work as expected", []string{"patch/patch-10.yaml"}, "deployment-1.json", "patch-10-deployment-1.txt"),
Entry("patch-10 on deployment-2 should work as expected", []string{"patch/patch-10.yaml"}, "deployment-2.json", "patch-10-deployment-2.txt"),
Entry("patch-10 on deployment-3 should work as expected", []string{"patch/patch-10.yaml"}, "deployment-3.json", "patch-10-deployment-3.txt"),
Entry("patch-11 on pod-6 should work as expected", []string{"patch/patch-11.yaml"}, "pod-6.json", "patch-11-pod-6.txt"),
Entry("patch-12 on pod-6 should work as expected", []string{"patch/patch-12.yaml"}, "pod-6.json", "patch-12-pod-6.txt"),
)

DescribeTable("DetermineRejections", modRuleStoreDetermineRejectionsTableFunction,
Expand Down
12 changes: 8 additions & 4 deletions core/modrulestoreitem.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type compiledJSONPatchOperation struct {
type patchPathItem struct {
path string
selectKeyParts []interface{}
selectedItem interface{}
}

var (
Expand Down Expand Up @@ -99,7 +100,7 @@ func (f *ModRuleStoreItemFactory) NewModRuleStoreItem(modRule *v1beta1.ModRule)
var rejectMessageTemplate *template.Template

if modRule.Spec.RejectMessage != nil {
rejectMessageTemplate, err = template.New("rejectMessage").Parse(*modRule.Spec.RejectMessage)
rejectMessageTemplate, err = util.NewSafeTemplate("rejectMessage").Parse(*modRule.Spec.RejectMessage)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -180,7 +181,7 @@ func newCompiledJSONPatch(patch []v1beta1.PatchOperation, jsonPathLanguage *gval
pathSprintfTemplate := pathTemplateToSprintfTemplate(po.Path)

// Compile the go template value.
tpl, err := template.New(po.Path).Parse(util.PreProcessModRuleGoTemplate(value))
tpl, err := util.NewSafeTemplate(po.Path).Parse(util.PreProcessModRuleGoTemplate(value))

if err != nil {
return nil, err
Expand Down Expand Up @@ -228,7 +229,7 @@ func (si *ModRuleStoreItem) calculatePatch(templateContext *PatchTemplateContext
return nil, err
}

for key := range result.(map[string]interface{}) {
for key, val := range result.(map[string]interface{}) {
selectKeyParts := keyPartsFromSelectKey(key)
path := pathFromKeyParts(selectKeyParts, cop.pathSprintfTemplate)

Expand All @@ -239,22 +240,25 @@ func (si *ModRuleStoreItem) calculatePatch(templateContext *PatchTemplateContext
pathItems = append(pathItems, patchPathItem{
path: path,
selectKeyParts: selectKeyParts,
selectedItem: val,
})
}
} else {
// No select expression? Add a simple path with no select key parts.
pathItems = append(pathItems, patchPathItem{
path: cop.path,
selectKeyParts: []interface{}{},
selectedItem: nil,
})
}

for _, pathItem := range pathItems {

vb := strings.Builder{}

// Bake in the select-key parts into the template context.
// Bake in the select-key parts and selected item into the template context.
templateContext.SelectKeyParts = pathItem.selectKeyParts
templateContext.SelectedItem = pathItem.selectedItem

err := cop.valueTemplate.Execute(&vb, templateContext)

Expand Down
3 changes: 3 additions & 0 deletions core/templatecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type PatchTemplateContext struct {

// SelectKeyParts contains the indexes collected from the patch select operation.
SelectKeyParts []interface{}

// SelectedItem is a reference to the current item resulting from executing the select expression.
SelectedItem interface{}
}

// RejectTemplateContext is an internal structure which is passed as context to all reject template executions.
Expand Down
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-11-pod-6.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{replace /spec/containers/0/image my-repo/nginx:1.14.2} {replace /spec/containers/2/image my-repo/nginx:1.15.2}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-12-pod-6.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{replace /spec/containers/0/image my-repo/nginx:1.14.2} {replace /spec/containers/2/image my-repo/nginx:1.15.2}]
16 changes: 16 additions & 0 deletions core/testdata/modrules/patch/patch-11.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch

match:
- select: '$.kind'
matchValue: 'Pod'

patch:
- op: replace
select: '$.spec.containers[? @.image =~ "repo1/.+"]'
path: /spec/containers/#0/image
value: '{{ regexReplaceAll "(.+)/(.*)" (index .SelectedItem "image") "my-repo/${2}" }}'
16 changes: 16 additions & 0 deletions core/testdata/modrules/patch/patch-12.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch

match:
- select: '$.kind'
matchValue: 'Pod'

patch:
- op: replace
select: '$.spec.containers[? @.image =~ "repo1/.+"].image'
path: /spec/containers/#0/image
value: '{{ regexReplaceAll "(.+)/(.*)" .SelectedItem "my-repo/${2}" }}'
Loading

0 comments on commit e96f279

Please sign in to comment.