Validation using JSON queries
An earlier section shows how to write a validation policy by using Go types describing Kubernetes objects.
There is another way to write validation logic, by extracting the relevant data from the JSON document using ad-hoc queries.
This "jq-like" approach can be useful when the policy has to look deep inside a Kubernetes object. It’s especially helpful when dealing with optional inner objects.
This document re-implements the earlier code using JSON queries instead of unmarshaling the JSON payload into Go types.
The validate
function
You can use the policy you just created and change its validate
function
to not use the Go types that define Kubernetes objects.
You can instead use the gjson
library to extract data from the raw JSON object.
Firstly, you need to change the requirement section. This is how the code should look:
import (
"encoding/json"
"fmt"
mapset "github.com/deckarep/golang-set/v2"
kubewarden "github.com/kubewarden/policy-sdk-go"
kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
"github.com/tidwall/gjson"
)
Change the validate
function to look like:
validate
function
func validate(payload []byte) ([]byte, error) {
// Create a ValidationRequest instance from the incoming payload
validationRequest := kubewarden_protocol.ValidationRequest{}
err := json.Unmarshal(payload, &validationRequest)
if err != nil {
return kubewarden.RejectRequest(
kubewarden.Message(err.Error()),
kubewarden.Code(400))
}
// Create a Settings instance from the ValidationRequest object
settings, err := NewSettingsFromValidationReq(&validationRequest)
if err != nil {
return kubewarden.RejectRequest(
kubewarden.Message(err.Error()),
kubewarden.Code(400))
}
// Access the **raw** JSON that describes the object
podJSON := validationRequest.Request.Object
// highlight-next-line
// NOTE 1
data := gjson.GetBytes(
podJSON,
"metadata.labels")
var validationErr error
labels := mapset.NewThreadUnsafeSet[string]()
data.ForEach(func(key, value gjson.Result) bool {
// highlight-next-line
// NOTE 2
label := key.String()
labels.Add(label)
// highlight-next-line
// NOTE 3
validationErr = validateLabel(label, value.String(), &settings)
// keep iterating if there are no errors
return validationErr == nil
})
// highlight-next-line
// NOTE 4
if validationErr != nil {
return kubewarden.RejectRequest(
kubewarden.Message(validationErr.Error()),
kubewarden.NoCode)
}
// highlight-next-line
// NOTE 5
for requiredLabel := range settings.ConstrainedLabels {
if !labels.Contains(requiredLabel) {
return kubewarden.RejectRequest(
kubewarden.Message(fmt.Sprintf("Constrained label %s not found inside of Pod", requiredLabel)),
kubewarden.NoCode)
}
}
return kubewarden.AcceptRequest()
}
The first part of the validate
function is similar as before.
'NOTE' highlights the changes.
-
You use a
gjson
selector to get thelabel
map provided by the object embedded into the request -
You use a
gjson
helper to iterate over the results of the query. If the query has no results, the loop never takes place. -
You use the
validateLabel
function to validate the label and its value, as before. You’re also adding the labels found in the Pod to a previously definedmapset.Set
. -
If the validation produced an error, you immediately return with a validation rejection reply.
-
As before, you iterate over the
constrainedLabels
to check all are specified in the Pod. The code has been slightly changed to make use of the previously populatedmapset.Set
.
Testing the validation code
The unit tests and the end-to-end tests don’t need any change, you can run them as before:
make test
go test -v
=== RUN TestParseValidSettings
--- PASS: TestParseValidSettings (0.00s)
=== RUN TestParseSettingsWithInvalidRegexp
--- PASS: TestParseSettingsWithInvalidRegexp (0.00s)
=== RUN TestDetectValidSettings
--- PASS: TestDetectValidSettings (0.00s)
=== RUN TestDetectNotValidSettingsDueToBrokenRegexp
--- PASS: TestDetectNotValidSettingsDueToBrokenRegexp (0.00s)
=== RUN TestDetectNotValidSettingsDueToConflictingLabels
--- PASS: TestDetectNotValidSettingsDueToConflictingLabels (0.00s)
=== RUN TestValidateLabel
--- PASS: TestValidateLabel (0.00s)
PASS
ok github.com/kubewarden/go-policy-template 0.002s
make e2e-tests
bats e2e.bats
e2e.bats
✓ accept when no settings are provided
✓ accept because label is satisfying a constraint
✓ accept labels are not on deny list
✓ reject because label is on deny list
✓ reject because label is not satisfying a constraint
✓ reject because constrained label is missing
✓ fail settings validation because of conflicting labels
✓ fail settings validation because of invalid constraint
8 tests, 0 failures
All the tests are working as expected.