Golang Kubernetes JSONPath
I’ve been spending some time learning Akri.
One proposal was to develop a webhook handler to check the YAML of Akri’s Configurations (CRDs). Configurations are used to describe Akri Brokers. They combine a Protocol reference (e.g. zeroconf) with a Kubernetes PodSpec (one of more containers), one of which references(using .resources.limits.{{PLACEHOLDER}}) the Akri device to be bound to the broker.
In order to validate the Configuration, one of Akri’s developers proposed using JSONPAth as a way to ‘query’ Kubernetes configuration files. This is a clever suggestion.
NOTE Curiously, although YAML is the preferred Kubernetes configuration file format, Kubernetes provides a JSON-based (there’s no YAML-based) query mechanism.
Kubernetes Golang client libraries includes a JSONPath implementation. This is poorly documented but the accompanying tests were very helpful in understanding how to use it.
This snippet describes how to use Kubernetes’ JSONPath functionality.
Here’s an example Akri Configuration:
{
"apiVersion": "akri.sh/v0",
"kind": "Configuration",
"metadata": {},
"spec": {
"brokerPodSpec": {
"containers": [{
"image": "image",
"name": "broker",
"resources": {
"limits": {
"{{PLACEHOLDER}}": "3"
}
}
}, {
"image": "sidecar",
"name": "name",
"resources": {
"limits": {
"cpu": "500m",
"memory": "128Mi"
},
"requests":{
"cpu": "250m",
"memory": "64Mi"
}
}
}]
},
"capacity": 1,
"protocol": {}
}
}
NOTE I’ve added a fake sidecar which requests resources too
There’s a trick using the Kubernetes JSONPath library, you must unmarshal into interface{} not into a Golang struct (representing your JSON).
The library returns string results from Execute; there’s a FindResults method that I’ve not (yet) used.
Variant #1
var v interface{}
if err := json.Unmarshal(raw, &v); err != nil {
log.Printf("[serve] Unable to unmarshal akri.sh/v0/Configuration: %+v", err)
return
}
j := jsonpath.New("limits")
// Here's the JSONPath filter #1
t := `{.spec.brokerPodSpec.containers[*].resources.limits}`
if err := j.Parse(t); err != nil {
log.Printf("[serve] Unable to parse JSONPath: %s", t)
return
}
b := new(bytes.Buffer)
if err := j.Execute(b, v); err != nil {
log.Printf("[serve] Unable to apply JSONPath to Configuration: %+v", err)
return
}
got := b.String()
key := "{{PLACEHOLDER}}"
if !strings.Contains(got, key) {
log.Printf("[serve] Configuration does not include: `%s[%s]`", t, key)
return
}
This queries for the .resources.limits in any container and will find 2 results for name=="broker" and name=="sidecar"
It then checks whether the string returned from the filter includes `{{PLACEHOLDER}}
Variant #2
This time the filter includes the (escaped) {{PLACEHOLDER}} key and checks whether the value is an integer.
var v interface{}
if err := json.Unmarshal(raw, &v); err != nil {
log.Printf("[serve] Unable to unmarshal akri.sh/v0/Configuration: %+v", err)
return
}
j := jsonpath.New("limits")
// Here's the JSONPath filter #2
t := `{.spec.brokerPodSpec.containers[*].resources.limits.\{\{PLACEHOLDER\}\}}`
if err := j.Parse(t); err != nil {
log.Printf("[serve] Unable to parse JSONPath: %s", t)
return
}
b := new(bytes.Buffer)
if err := j.Execute(b, v); err != nil {
log.Printf("[serve] Unable to apply JSONPath to Configuration: %+v", err)
return
}
got := b.String()
if _, err := strconv.Atoi(got); err != nil {
t.Errorf("[test] got: %s; want: a string containing an integer", got)
}
That’s all!