Skip to content
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

Implement support for undefined values in JSONPath #88

Merged
merged 5 commits into from
Apr 11, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## Unreleased

- 87: Extend JSPONPath with select functions isUndefined, isDefined, isEmpty and isNotEmpty
- 70: Add support for ARM64

## 0.14.0 - 2022-01-29
Expand Down
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ all: manager

# Run tests
test: generate fmt vet manifests
go test ./core ./util -coverprofile cover.out
go test ./core ./util ./jsonpath -coverprofile cover.out

# Run tests -v
testv: generate fmt vet manifests
go test -v ./core ./util -coverprofile cover.out

go test -v ./core ./util ./jsonpath -coverprofile cover.out

# Run benchmarks
bench: generate fmt vet manifests
Expand Down
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Use KubeMod to:
* [Target resources](#target-resources)
* [Note on idempotency](#note-on-idempotency)
* [Debugging ModRules](#debugging-modrules)
* [KubeMod's version of JSONPath](#kubemods-version-of-jsonpath)
* [Declarative kubectl apply](#declarative-kubectl-apply)
* [Gotchas](#gotchas)

Expand Down Expand Up @@ -431,6 +432,8 @@ A criteria item whose `select` expression yields no results is considered non-ma

The `select` field of a criteria item is a [JSONPath](https://goessner.net/articles/JsonPath/) expression.

[See more on KubeMod's version of JSONPath](#kubemods-version-of-jsonpath).

When a `select` expression is evaluated against a Kubernetes object definition, it yields zero or more values.

Let's consider the following JSONPath select expression:
Expand Down Expand Up @@ -993,6 +996,85 @@ When an object is patched by a ModRule, if the object has a `kubectl.kubernetes.

KubeMod supports both client-side and server-side declarative management through `kubectl apply`.

### KubeMod's version of JSONPath

KubeMod implements a modified (extended) version of [JSONPath](https://goessner.net/articles/JsonPath/).

This version introduces the following new features:

#### Value `undefined`
- Includes internal representation of value `undefined`.
- Resolves each path that includes undefined properties to value `undefined`.
For example `$.a.b.c` will resolve to `undefined` if any of the `a`, `b` or `c` properties do not exist.
- Filters out all `undefined` values on partial matches.
- Makes all equality and arithmetic comparisons to `undefined` return `false`.
For example, assuming that `$.a.b.c` is a path to undefined property, all of the following expressions will yield `false`:
- `$.a.b.c == 12`
- `$.a.b.c != 12`
- `$.a.b.c > 12`
- `$.a.b.c < 12`
- `$.a.b.c == true`
- `$.a.b.c == false`
- Makes all boolean operators (`&&` and `||`) require boolean operands.

#### `undefined`-based functions
- `isDefined()` - returns `true` if the passed in path leads to a defined property, otherwise return `false`.
- `isUndefined()` - returns `true` if the passed in path leads to an undefined property, otherwise return `false`.
- `isEmpty()` - returns `true` if the passed in path leads to one of the following values:
- An empty array
- An empty object
- An empty string
- Null
- `undefined`
- `isNotEmpty()` - equivalent to evaluating `!isEmpty()`
- `length()` - returns the length of arrays, objects and strings. Returns `0` for Nulls and `undefined`.

#### Note on presence check

KubeMod uses the above `undefined` based functions to provide both presence (`isDefined`) and negative-presence (`isUndefined`) filters - see next section for an example.

These functions should be used in place of the standard JSONPath's presence-based `[?(@.property)]` filter [discussed here](https://goessner.net/articles/JsonPath/).

#### Usage in ModRules

For example, to patch all deployments' containers which have either no `securityContext` defined, or `securityContext` is empty, one would use the following KubeMod rule.

```yaml
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: kubemod-patch-deployments-containers-securitycontext
namespace: kubemod-system
spec:
targetNamespaceRegex: .*
type: Patch
match:
- matchValue: Deployment
select: $.kind
patch:
- op: add
select: '$.spec.template.spec.containers[? isEmpty(@.securityContex)]'
path: '/spec/template/spec/containers/#0/securityContext'
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
```
The rule uses `isEmpty` which returns `true` when the passed in path is not defined or if it points to an empty object.

If we wanted to only patch the containers which have no `securityContex` defined, but leave the ones which have an empty `securityContex`, we would use the following `select`:

```yaml
select: '$.spec.template.spec.containers[? isUndefined(@.securityContex)]'
```

If we wanted to only patch the containers which have an empty `securityContex`, but leave the ones which have no `securityContex` defined, we would use the following `select`:

```yaml
select: '$.spec.template.spec.containers[? isDefined(@.securityContex) && isEmpty(@.securityContex)]'
```

### Gotchas

When multiple ModRules match the same resource object, all of the ModRule patches are executed against the object in an indeterminate order.
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/modrule_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
// log is for logging in this package.
var (
modrulelog = logf.Log.WithName("modrule-resource")
jsonPathLanguage = expressions.NewJSONPathLanguage()
jsonPathLanguage = expressions.NewKubeModJSONPathLanguage()
)

// SetupWebhookWithManager hooks up the web hook with a manager.
Expand Down
4 changes: 2 additions & 2 deletions app/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func InitializeKubeModOperatorApp(
enableLeaderElection EnableLeaderElection,
log logr.Logger) (*KubeModOperatorApp, error) {
wire.Build(
expressions.NewJSONPathLanguage,
expressions.NewKubeModJSONPathLanguage,
core.NewModRuleStoreItemFactory,
core.NewModRuleStore,
core.NewDragnetWebhookHandler,
Expand All @@ -56,7 +56,7 @@ func InitializeKubeModWebApp(
clusterModRulesNamespace core.ClusterModRulesNamespace,
log logr.Logger) (*KubeModWebApp, error) {
wire.Build(
expressions.NewJSONPathLanguage,
expressions.NewKubeModJSONPathLanguage,
core.NewModRuleStoreItemFactory,
NewKubeModWebApp,
)
Expand Down
4 changes: 2 additions & 2 deletions app/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions core/modrulestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ var _ = Describe("ModRuleStore", func() {
Entry("patch-13 on deployment-2 should work - ModRule in kubemod-system and targetNamespaceRegex matches", []string{"patch/patch-13.yaml"}, "deployment-2.json", "patch-13-deployment-2.txt"),
Entry("patch-14 on deployment-2 should not work - ModRule in kubemod-system, missing targetNamespaceRegex", []string{"patch/patch-14.yaml"}, "deployment-2.json", "empty-array.txt"),
Entry("patch-15 on deployment-2 should not work - ModRule in kubemod-system, empty targetNamespaceRegex", []string{"patch/patch-15.yaml"}, "deployment-2.json", "empty-array.txt"),
Entry("patch-16 on deployment-4 should work as expected", []string{"patch/patch-16.yaml"}, "deployment-4.json", "patch-16-deployment-4.txt"),
Entry("patch-17 on deployment-4 should work as expected", []string{"patch/patch-17.yaml"}, "deployment-4.json", "patch-17-deployment-4.txt"),
Entry("patch-18 on deployment-4 should work as expected", []string{"patch/patch-18.yaml"}, "deployment-4.json", "patch-18-deployment-4.txt"),
Entry("patch-19 on deployment-4 should work as expected", []string{"patch/patch-19.yaml"}, "deployment-4.json", "patch-19-deployment-4.txt"),
Entry("patch-20 on deployment-4 should work as expected", []string{"patch/patch-20.yaml"}, "deployment-4.json", "patch-20-deployment-4.txt"),
Entry("patch-21 on deployment-4 should work as expected", []string{"patch/patch-21.yaml"}, "deployment-4.json", "patch-21-deployment-4.txt"),
)

DescribeTable("DetermineRejections", modRuleStoreDetermineRejectionsTableFunction,
Expand Down
2 changes: 2 additions & 0 deletions core/testdata/expectations/patch-16-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

[{add /spec/template/spec/containers/0/securityContext map[capabilities:map[drop:[ALL]] runAsNonRoot:true]} {add /spec/template/spec/containers/1/securityContext/capabilities map[drop:[ALL]]} {add /spec/template/spec/containers/1/securityContext/runAsNonRoot true}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-17-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{add /spec/template/spec/containers/0/securityContext map[capabilities:map[drop:[ALL]] runAsNonRoot:true]}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-18-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{add /spec/template/spec/containers/1/securityContext/capabilities map[drop:[ALL]]} {add /spec/template/spec/containers/1/securityContext/runAsNonRoot true} {add /spec/template/spec/containers/2/securityContext/capabilities map[drop:[ALL]]} {remove /spec/template/spec/containers/2/securityContext/allowPrivilegeEscalation <nil>}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-19-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{add /spec/template/spec/containers/2/securityContext/capabilities map[drop:[ALL]]} {remove /spec/template/spec/containers/2/securityContext/allowPrivilegeEscalation <nil>}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-20-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{add /spec/template/spec/containers/0/securityContext map[capabilities:map[drop:[ALL]] runAsNonRoot:true]} {add /spec/template/spec/containers/2/securityContext/capabilities map[drop:[ALL]]} {remove /spec/template/spec/containers/2/securityContext/allowPrivilegeEscalation <nil>}]
1 change: 1 addition & 0 deletions core/testdata/expectations/patch-21-deployment-4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{add /spec/template/spec/containers/1/securityContext/capabilities map[drop:[ALL]]} {add /spec/template/spec/containers/1/securityContext/runAsNonRoot true}]
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-16.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isEmpty(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-17.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isUndefined(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-18.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isDefined(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-19.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isNotEmpty(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-20.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isUndefined(@.securityContext) || isNotEmpty(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
22 changes: 22 additions & 0 deletions core/testdata/modrules/patch/patch-21.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: api.kubemod.io/v1beta1
kind: ModRule
metadata:
name: modrule-1
spec:
type: Patch
# optional targetNamespaceRegex missing -- when missing, rules in
# kubemod-system namespace can only target non-namespaced resources

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

patch:
- op: add
select: '$.spec.template.spec.containers[? isDefined(@.securityContext) && isEmpty(@.securityContext)]'
path: /spec/template/spec/containers/#0/securityContext
value: |-
runAsNonRoot: true
capabilities:
drop:
- ALL
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ spec:
- select: "$.kind"
matchValue: Pod

- select: "$.spec.securityContext.runAsNonRoot == true"
- select: '$.spec.securityContext.runAsNonRoot == true'
negate: true
Loading