Validation Reference
Validation Struct
The Validation
struct is a data structure used for ingesting validation data. It contains the following fields:
LulaVersion
(string): Optional field to maintain backward compatibility.Metadata
(*Metadata): Optional metadata containing the name and UUID of the validation.Provider
(*Provider): Required field specifying the provider and its corresponding specification.Domain
(*Domain): Required field specifying the domain and its corresponding specification.
The Metadata
struct contains the following fields:
Name
(string): Optional short description to use in the output of validations.UUID
(string): Optional UUID of the validation.
Domain Struct
The Domain
struct contains the following fields:
Type
(string): Required field specifying the type of domain (enum: kubernetes
, api
).KubernetesSpec
(*KubernetesSpec): Optional specification for a Kubernetes domain, required if type is kubernetes
.ApiSpec
(*ApiSpec): Optional specification for an API domain, required if type is api
.
Provider Struct
The Provider
struct contains the following fields:
Type
(string): Required field specifying the type of provider (enum: opa
, kyverno
).OpaSpec
(*OpaSpec): Optional specification for an OPA provider.KyvernoSpec
(*KyvernoSpec): Optional specification for a Kyverno provider.
Example YAML Document
The following is an example of a YAML document for a validation artifact:
lula-version: ">=v0.2.0"
metadata:
name: Validate pods with label foo=bar
uuid: 123e4567-e89b-12d3-a456-426655440000
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package validate
import future.keywords.every
validate {
every pod in input.podsvt {
podLabel := pod.metadata.labels.foo
podLabel == "bar"
}
}
Linting
Linting is done by Lula when a Validation
object is converted to a LulaValidation
for evaluation.
The common.Validation.Lint
method is a convenience method to lint a Validation
object. It performs the following step:
- Marshalling: The method marshals the
Validation
object into a YAML byte array using the common.Validation.MarshalYaml
function. - Linting: The method runs linting against the marshalled
Validation
object. This is done using the schemas.Validate
function, which ensures that the YAML data conforms to the expected schema.
The schemas.Validate
function is responsible for validating the provided data against a specified JSON schema using github.com/santhosh-tekuri/jsonschema/v6. The process involves the following steps:
- Coercion to JSON Map: The provided data, which can be either an interface or a byte array, is coerced into a JSON map using the
model.CoerceToJsonMap
function. - Schema Retrieval: The function retrieves the JSON schema specified by the
schema
parameter using the GetSchema
function. - Schema Compilation: The retrieved schema is compiled into a format that can be used for validation using the
jsonschema.CompileString
function. - Validation: The coerced JSON map is validated against the compiled schema. If the validation fails, the function extracts the specific errors and returns them as a formatted string.
VS Code intellisense:
- Ensure that the YAML (Red Hat) extension is installed.
- Add the following to your settings.json:
"yaml.schemas": {
"${PATH_TO_LULA}/lula/src/pkg/common/schemas/validation.json": "*validation*.yaml"
},
Note:
${PATH_TO_LULA}
should be replaced with your path.*validation*.yaml
may be changed to match your project’s validation file naming conventions.- can also be limited to project or workspace settings if desired
1 -
Configuration
Lula allows the use and specification of a config file in the following ways:
- Checking current working directory for a
lula-config.yaml
file - Specification with environment variable
LULA_CONFIG=<path>
Environment Variables can be used to specify configuration values through use of LULA_<VAR>
-> Example: LULA_TARGET=il5
Identification
If identified, Lula will log which configuration file is used to stdout:
Using config file /home/dev/work/lula/lula-config.yaml
Precedence
The precedence for configuring settings, such as target
, follows this hierarchy:
Command Line Flag > Environment Variable > Configuration File
Command Line Flag:
When a setting like target
is specified using a command line flag, this value takes the highest precedence, overriding any environment variable or configuration file settings.
Environment Variable:
If the setting is not provided via a command line flag, an environment variable (e.g., export LULA_TARGET=il5
) will take precedence over the configuration file.
Configuration File:
In the absence of both a command line flag and environment variable, the value specified in the configuration file will be used. This will override system defaults.
Support
Modification of command variables can be set in the configuration file:
lula-config.yaml
log_level: debug
target: il4
summary: true
2 -
Domains
Domains, generically, are the areas or categories from which data is collected to be evaluated by a Lula Validation
. Currently supported Domains are:
The domain block of a Lula Validation
is given as follows, where the sample is indicating a Kubernetes domain is in use:
# ... Rest of Lula Validation
domain:
type: kubernetes # kubernetes or api accepted
kubernetes-spec:
# ... Rest of kubernetes-spec
# ... Rest of Lula Validation
Each domain has a particular specification, given by the respective <domain>-spec
field of the domain
property of the Lula Validation
. The sub-pages describe each of these specifications in greater detail.
2.1 -
API Domain
The API Domain allows for collection of data generically from API endpoints.
2.2 -
Kubernetes Domain
The Kubernetes domain provides Lula access to data collection of Kubernetes resources.
[!Important]
This domain supports both read and write operations on the Kubernetes cluster in the current context, so use with care
Specification
The Kubernetes specification can be used to return Kubernetes resources as JSON data to the providers. The specification allows for both manifest and resource field data to be retreived.
The following sample kubernetes-spec
yields a list of all pods in the validation-test
namespace.
domain:
type: kubernetes
kubernetes-spec:
resources: # Optional - Group of resources to read from Kubernetes
- name: podsvt # Required - Identifier to the list or set read by the policy
resource-rule: # Required - Resource selection criteria, at least one resource rule is required
name: # Optional - Used to retrieve a specific resource in a single namespace
group: # Optional - empty or "" for core group
version: v1 # Required - Version of resource
resource: pods # Required - Resource type (API-recognized type, not Kind)
namespaces: [validation-test] # Optional - Namespaces to validate the above resources in. Empty or "" for all namespace or non-namespaced resources
field: # Optional - Field to grab in a resource if it is in an unusable type, e.g., string json data. Must specify named resource to use.
jsonpath: # Required - Jsonpath specifier of where to find the field from the top level object
type: # Optional - Accepts "json" or "yaml". Default is "json".
base64: # Optional - Boolean whether field is base64 encoded
[!Tip]
Lula supports eventual-consistency through use of an optional wait
field in the kubernetes-spec
.
domain:
type: kubernetes
kubernetes-spec:
wait: # Optional - Group of resources to read from Kubernetes
condition: Ready # ...
kind: pod/test-pod-wait # ...
namespace: validation-test # ...
timeout: 30s # ...
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
Resource Creation
The Kubernetes domain also supports creating, reading, and destroying test resources in the cluster. This feature should be used with caution since it’s writing to the cluster and ideally should be implemented on separate namespaces to make any erroneous outcomes easier to mitigate.
domain:
type: kubernetes
kubernetes-spec:
create-resources: # Optional - Group of resources to be created/read/destroyed in Kubernetes
- name: testPod # Required - Identifier to be read by the policy
namespace: validation-test # Optional - Namespace to be created if applicable (no need to specify if ns exists OR resource is non-namespaced)
manifest: | # Optional - Manifest string for resource(s) to create; Only optional if file is not specified
<some manifest(s)>
file: '<some url>' # Optional - File name where resource(s) to create are stored; Only optional if manifest is not specified
Lists vs Named Resource
When Lula retrieves all targeted resources (bounded by namespace when applicable), the payload is a list of resources. When a resource Name is specified - the payload will be a single object.
Example
Let’s get all pods in the validation-test
namespace and evaluate them with the OPA provider:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package validate
import future.keywords.every
validate {
every pod in input.podsvt {
podLabel := pod.metadata.labels.foo
podLabel == "bar"
}
}
[!IMPORTANT]
Note how the rego evaluates a list of items that can be iterated over. The podsvt
field is the name of the field in the kubernetes-spec.resources that contains the list of items.
Now let’s retrieve a single pod from the validation-test
namespace:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podvt
resource-rule:
name: test-pod-label
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package validate
validate {
podLabel := input.podvt.metadata.labels.foo
podLabel == "bar"
}
[!IMPORTANT]
Note how the rego now evaluates a single object called podvt
. This is the name of the resource that is being validated.
We can also retrieve a single cluster-scoped resource as follows, where the rego evaluates a single object called namespaceVt
.
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: namespaceVt
resource-rule:
name: validation-test
version: v1
resource: namespaces
provider:
type: opa
opa-spec:
rego: |
package validate
validate {
input.namespaceVt.metadata.name == "validation-test"
}
Many of the tool-specific configuration data is stored as json or yaml text inside configmaps and secrets. Some valuable data may also be stored in json or yaml strings in other resource locations, such as annotations. The field
parameter of the resource-rule
allows this data to be extracted and used by the Rego.
Here’s an example of extracting config.yaml
from a test configmap:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: configdata
resource-rule:
name: test-configmap
group:
version: v1
resource: configmaps
namespaces: [validation-test]
field:
jsonpath: .data.my-config.yaml
type: yaml
provider:
type: opa
opa-spec:
rego: |
package validate
validate {
configdata.configuration.foo == "bar"
}
Where the raw ConfigMap data would look as follows:
configuration:
foo: bar
anotherValue:
subValue: ba
This field also supports grabbing secret data using the optional base64
field as follows
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: secretdata
resource-rule:
name: test-secret
group:
version: v1
resource: secrets
namespaces: [validation-test]
field:
jsonpath: .data.secret-value.json
type: json
base64: true
3 -
Providers
Providers are the policy engines which evaluate the input data from the specified domain. Currently supported Providers are:
- OPA (Open Policy Agent)
- Kyverno
The provider block of a Lula Validation
is given as follows, where the sample is indicating the OPA provider is in use:
# ... Rest of Lula Validation
provider:
type: opa # opa or kyverno accepted
opa-spec:
# ... Rest of opa-spec
# ... Rest of Lula Validation
Each domain specification retreives a specific dataset, and each will return that data to the selected Provider
in a domain-specific format. However, this data will always take the form of a JSON object when input to a Provider
. For that reason, it is important that Domain
and Provider
specifications are not built wholly independently in a given Validation.
3.1 -
Kyverno Provider
The Kyverno provider provides Lula with the capability to evaluate the domain
in against a Kyverno policy.
Payload Expectation
The validation performed should use the form of provider with the type
of kyverno
and using the kyverno-spec
, along with a valid domain.
Example:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: kyverno
kyverno-spec:
policy:
apiVersion: json.kyverno.io/v1alpha1 # Required
kind: ValidatingPolicy # Required
metadata:
name: pod-policy # Required
spec:
rules:
- name: no-latest # Required
# Match payloads corresponding to pods
match: # Optional
any: # Assertion Tree
- apiVersion: v1
kind: Pod
assert: # Required
all: # Assertion Tree
- message: Pod `{{ metadata.name }}` uses an image with tag `latest`
check:
~.podsvt:
spec:
# Iterate over pod containers
# Note the `~.` modifier, it means we want to iterate over array elements in descendants
~.containers:
image:
# Check that an image tag is present
(contains(@, ':')): true
# Check that the image tag is not `:latest`
(ends_with(@, ':latest')): false
You can have mutiple policies defined. Optionally, output.validation
can be specified in the kyverno-spec
to control which (Policy, Rule) pair control validation allowance/denial, which is in the structure of a comma separated list of rules: policy-name1.rule-name-1,policy-name-1.rule-name-2
. If you have a desired observation to include, output.observations
can be added to payload to observe violations by a certain (Policy, Rule) pair such as:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: kyverno
kyverno-spec:
policy:
apiVersion: json.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: labels
spec:
rules:
- name: foo-label-exists
assert:
all:
- check:
~.podsvt:
metadata:
labels:
foo: bar
output:
validation: labels.foo-label-exists
observations:
- labels.foo-label-exists
The validatation
and observations
fields must specify a (Policy, Rule) pair. These observations will be printed out in the remarks
section of relevant-evidence
in the assessment results.
3.2 -
OPA Provider
The OPA provider provides Lula with the capability to evaluate the domain
against a rego policy.
Payload Expectation
The validation performed should use the form of provider with the type
of opa
and using the opa-spec
, along with a valid domain.
Example:
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: | # Required - Rego policy used for data validation
package validate # Required - Package name
import future.keywords.every # Optional - Any imported keywords
validate { # Required - Rule Name for evaluation - "validate" is the only supported rule
every pod in input.podsvt {
podLabel == "bar"
}
}
Optionally, an output
can be specified in the opa-spec
. Currently, the default validation allowance/denial is given by validate.validate
, which is really of the structure <package-name>.<json-path-to-boolean-variable>
. If you have a desired alternative validation boolean variable, as well as additional observations to include, an output can be added such as:
domain:
type: kubernetes
kubernetes-spec:
resource-rules:
- group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package mypackage
result {
input.kind == "Pod"
podLabel := input.metadata.labels.foo
podLabel == "bar"
}
test := "my test string"
output:
validation: mypackage.result
observations:
- validate.test
The validatation
field must specify a json path that resolves to a boolean value. The observations
array currently only support variables that resolve as strings. These observations will be printed out in the remarks
section of relevant-evidence
in the assessment results.
Policy Creation
The required structure for writing a validation in rego for Lula to validate is as follows:
rego: |
package validate
validate {
}
This structure can be utilized to evaluate an expression directly:
rego: |
package validate
validate {
input.kind == "Pod"
podLabel := input.metadata.labels.foo
podLabel == "bar"
}
The expression can also use multiple rule bodies
as such:
rego: |
package validate
foolabel {
input.kind == "Pod"
podLabel := input.metadata.labels.foo
podLabel == "bar"
}
validate {
foolabel
}
[!IMPORTANT]
package validate
and validate
are required package and rule for Lula use currently when an output.validation value has not been set.
4 -
Version Specification
In cases where a specific version of Lula is desired, either for typing constraints or desired functionality, a lula-version
property is recognized in the description
(component-definition.back-matter.resources[_]):
- uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C
remarks: >-
No outputs in payload
description: |
lula-version: ">=0.0.2"
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
rego: |
package validate
import future.keywords.every
validate {
every pod in input.podsvt {
podLabel == "bar"
}
}
If included, the lula-version
must be a string and should indicate the version constraints desired, if any. Our implementation uses Hashicorp’s go-version library, and constraints should follow their conventions.
If an invalid string is passed or the current Lula version does not meet version constraints, the implementation will automatically be marked “not-satisfied” and a remark will be created in the Assessment Report detailing the rationale.