Defining policy settings
Firstly, you need to define the structure that holds the policy settings.
You do this by modifying the code in the settings.go
file (from your local version of the Go policy template).
You need to add two extra lines to the import
section,
change the Settings
structure,
and add the RegularExpression
structure.
It should match the following code:
import (
"encoding/json"
"fmt"
"regexp"
mapset "github.com/deckarep/golang-set/v2"
kubewarden "github.com/kubewarden/policy-sdk-go"
kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
)
type Settings struct {
DeniedLabels mapset.Set[string] `json:"denied_labels"`
ConstrainedLabels map[string]*RegularExpression `json:"constrained_labels"`
}
type RegularExpression struct {
*regexp.Regexp
}
DeniedNames
is no longer required in the Settings
structure defined in settings.go
.
As DeniedNames
is no longer defined, you should also delete the function IsNameDefined
in settings.go
.
You should also remove the function that references it in settings_test.go
, TestIsNameDenied
.
You're using the regexp
package to handle regular expression objects and the
mapset.Set
type to store
the list of denied labels.
Since regexp.Regexp
doesn't handle deserialization,
you need to define custom functions to handle marshaling and unmarshalling of regular expressions:
// UnmarshalText satisfies the encoding.TextMarshaler interface,
// also used by json.Unmarshal.
func (r *RegularExpression) UnmarshalText(text []byte) error {
nativeRegExp, err := regexp.Compile(string(text))
if err != nil {
return err
}
r.Regexp = nativeRegExp
return nil
}
// MarshalText satisfies the encoding.TextMarshaler interface,
// also used by json.Marshal.
func (r *RegularExpression) MarshalText() ([]byte, error) {
if r.Regexp != nil {
return []byte(r.Regexp.String()), nil
}
return nil, nil
}
Also, you need the UnmarshalJSON
method to handle the deserialization of the Settings
struct:
func (s *Settings) UnmarshalJSON(data []byte) error {
rawSettings := struct {
DeniedLabels []string `json:"denied_labels"`
ConstrainedLabels map[string]*RegularExpression `json:"constrained_labels"`
}{}
err := json.Unmarshal(data, &rawSettings)
if err != nil {
return err
}
s.DeniedLabels = mapset.NewThreadUnsafeSet[string](rawSettings.DeniedLabels...)
s.ConstrainedLabels = rawSettings.ConstrainedLabels
return nil
}
Building Settings
instances​
A Kubewarden policy exposes two different functions that receive the policy settings as input:
validate
: Use this function when Kubernetes object requires validation by the policy. The settings are part of aValidationRequest
object.validate_settings
: Call this function when the policy is first loaded by Kubewarden. The function receives the policy settings as input and checks validity.
You need to create a helper function that creates a Settings
object starting from the JSON payload:
func NewSettingsFromValidationReq(validationReq *kubewarden_protocol.ValidationRequest) (Settings, error) {
settings := Settings{}
err := json.Unmarshal(validationReq.Settings, &settings)
if err != nil {
return Settings{}, err
}
return settings, nil
}
All the mapset.Set
objects are created using the
thread-unsafe variant.
The WebAssembly code executes in a single thread, hence there are no concurrency issues.
The WebAssembly standard doesn't cover threads yet. See the official proposal for more details.
Implementing Settings
validation​
All Kubewarden policies have to implement settings validation.
You do this by adding a Valid
method to the Settings
instances:
func (s *Settings) Valid() (bool, error) {
constrainedLabels := mapset.NewThreadUnsafeSet[string]()
for label := range s.ConstrainedLabels {
constrainedLabels.Add(label)
}
constrainedAndDenied := constrainedLabels.Intersect(s.DeniedLabels)
if constrainedAndDenied.Cardinality() != 0 {
return false,
fmt.Errorf("These labels cannot be constrained and denied at the same time: %v", constrainedAndDenied)
}
return true, nil
}
The Valid
method ensures no "denied" label is also part of the "constrained" map.
Usage of the Intersect
method provided by mapset.Set
simplifies the check.
The Valid
method invocation is on an already instantiated Setting
object.
This means the validation of the regular expression provided by the user already took place in of the Settings
unmarshaller.
Finally, you need the validateSettings
function,
provided by the scaffolding,
to change to look like this:
func validateSettings(payload []byte) ([]byte, error) {
settings := Settings{}
err := json.Unmarshal(payload, &settings)
if err != nil {
return kubewarden.RejectSettings(
kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
}
valid, err := settings.Valid()
if valid {
return kubewarden.AcceptSettings()
}
return kubewarden.RejectSettings(
kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
}
You can see the function takes advantage of the helper functions provided by Kubewarden's SDK.
Testing the settings code​
It's important to have good test coverage of the code you write.
The code you are using, from the scaffolding, comes with a series of unit tests defined in the settings_test.go
file.
You have to change the contents of this file to reflect the new behavior of the Settings
class.
Include the Go packages you are using:
import (
"testing"
"encoding/json"
kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
)
You can start by writing a unit test that ensures you can assign a Settings
instance from a ValidationRequest
object:
func TestParseValidSettings(t *testing.T) {
settingsJSON := []byte(`
{
"denied_labels": [ "foo", "bar" ],
"constrained_labels": {
"cost-center": "cc-\\d+"
}
}`)
settings := Settings{}
err := json.Unmarshal(settingsJSON, &settings)
if err != nil {
t.Errorf("Unexpected error %+v", err)
}
expected_denied_labels := []string{"foo", "bar"}
for _, exp := range expected_denied_labels {
if !settings.DeniedLabels.Contains(exp) {
t.Errorf("Missing value %s", exp)
}
}
re, found := settings.ConstrainedLabels["cost-center"]
if !found {
t.Error("Didn't find the expected constrained label")
}
expected_regexp := `cc-\d+`
if re.String() != expected_regexp {
t.Errorf("Expected regexp to be %v - got %v instead",
expected_regexp, re.String())
}
}
Next, you need a test that checks a Settings
instance isn't generated when the user provides a broken regular expression:
func TestParseSettingsWithInvalidRegexp(t *testing.T) {
settingsJSON := []byte(`
{
"denied_labels": [ "foo", "bar" ],
"constrained_labels": {
"cost-center": "cc-[a+"
}
}`)
err := json.Unmarshal(settingsJSON, &Settings{})
if err == nil {
t.Errorf("Didn't get expected error")
}
}
Now, you can define a test that checks the behavior of the
validate_settings
entry point.
You look at the SettingsValidationResponse
object returned by your validateSettings
function:
func TestDetectValidSettings(t *testing.T) {
settingsJSON := []byte(`
{
"denied_labels": [ "foo", "bar" ],
"constrained_labels": {
"cost-center": "cc-\\d+"
}
}`)
responsePayload, err := validateSettings(settingsJSON)
if err != nil {
t.Errorf("Unexpected error %+v", err)
}
var response kubewarden_protocol.SettingsValidationResponse
if err := json.Unmarshal(responsePayload, &response); err != nil {
t.Errorf("Unexpected error: %+v", err)
}
if !response.Valid {
t.Errorf("Expected settings to be valid: %s", *response.Message)
}
}
Finally, you write two more tests to check the validateSettings
function rejects invalid settings with the right messages:
func TestDetectNotValidSettingsDueToBrokenRegexp(t *testing.T) {
settingsJSON := []byte(`
{
"denied_labels": [ "foo", "bar" ],
"constrained_labels": {
"cost-center": "cc-[a+"
}
}
`)
responsePayload, err := validateSettings(settingsJSON)
if err != nil {
t.Errorf("Unexpected error %+v", err)
}
var response kubewarden_protocol.SettingsValidationResponse
if err := json.Unmarshal(responsePayload, &response); err != nil {
t.Errorf("Unexpected error: %+v", err)
}
if response.Valid {
t.Error("Expected settings to not be valid")
}
if *response.Message != "Provided settings are not valid: error parsing regexp: missing closing ]: `[a+`" {
t.Errorf("Unexpected validation error message: %s", *response.Message)
}
}
func TestDetectNotValidSettingsDueToConflictingLabels(t *testing.T) {
settingsJSON := []byte(`
{
"denied_labels": [ "foo", "bar", "cost-center" ],
"constrained_labels": {
"cost-center": ".*"
}
}`)
responsePayload, err := validateSettings(settingsJSON)
if err != nil {
t.Errorf("Unexpected error %+v", err)
}
var response kubewarden_protocol.SettingsValidationResponse
if err := json.Unmarshal(responsePayload, &response); err != nil {
t.Errorf("Unexpected error: %+v", err)
}
if response.Valid {
t.Error("Expected settings to not be valid")
}
if *response.Message != "Provided settings are not valid: These labels cannot be constrained and denied at the same time: Set{cost-center}" {
t.Errorf("Unexpected validation error message: %s", *response.Message)
}
}
Now you can run the tests that you have defined so far by using the following command:
go test -v settings.go settings_test.go
All the tests will pass with the following output:
=== 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)
PASS
ok command-line-arguments 0.002s
You can now implement the actual validation code in the next section.