@kubevious/kubik
v1.0.33
Published
Kubik - domain specific validation language
Downloads
498
Readme
Kubik
Kubik (pronounced [ku:bik]) is a Kubernetes-centric validation and query language for detecting and preventing errors and violations of compliance, security, and cloud-native best practices. It was designed from the ground up to allow graph-like queries across manifests. Kubik uses JavaScript-like syntax for expressing custom rules.
We are maintaining a community-driven rules library which is a great way to get familiar with Kubik and implement your own custom rules.
The Kubevious CLI command-line tool interprets Kubik rules towards manifests in the file system as well as live manifests already configured in the Kubernetes cluster.
Concepts
Kubik rule consists of two primary parts: target and rule scripts. The target script declares on which manifests the validation should be executed. The rule script validates each manifest that matches the target script.
Target:
Api('apps')
.Kind('Deployment')
Rule:
for(const container of config.spec?.template?.spec?.containers ?? [])
{
for(const envFrom of container.envFrom ?? [])
{
if (envFrom.configMapRef)
{
const configMap = ApiVersion('v1')
.Kind('ConfigMap')
.name(envFrom.configMapRef.name)
.single()
if (!configMap)
{
error(`Could not find ConfigMap ${envFrom.configMapRef.name}`)
}
}
}
}
Target Script
Querying K8s Manifests
Query Kubernetes objects by ApiVersion and Kind:
ApiVersion('apps/v1')
.Kind('Deployment')
Query Kubernetes object by ApiGroup and Kind:
Api('autoscaling')
.Kind('HorizontalPodAutoscaler')
Query Clustered objects:
Api('rbac.authorization.k8s.io')
.Kind('ClusterRoleBinding')
.isClusterScope(true)
Filtering query results by object name:
ApiVersion('v1')
.Kind('Service')
.name('backend')
Filtering query results by label:
ApiVersion('v1')
.Kind('Service')
.label('foo', 'bar')
Filtering query results by multiple labels:
ApiVersion('v1')
.Kind('Service')
.label({ foo: 'bar', stage: 'prod' })
Limiting the query within a namespace:
ApiVersion('v1')
.Kind('Service')
.namespace('pepsi')
Union
Queries can be combined:
Union(
Api('rbac.authorization.k8s.io')
.Kind("ClusterRoleBinding")
.isClusterScope(true),
Api('rbac.authorization.k8s.io')
.Kind("RoleBinding")
)
Shortcut
Kubik comes with Shortcut queries to simplify writing common Kubernetes rules:
| Shortcut | Description | | ------------------------- | ------------------------------------------------------------ | | Shortcut('PodSpec') | Returns PodSpecs across Deployments, StatefulSets, DaemonSet, Jobs, and CronJobs | | Shortcut('ContainerSpec') | Returns ContainerSpecs across Deployments, StatefulSets, DaemonSet, Jobs, and CronJobs. Also includes init containers. | | Shortcut('Secret', name) | Returns the K8s Secret object or the Secret object produced by the corresponding bitnami SealedSecret. |
Filter
Queries can be filtered:
Filter(
Api('batch')
.Kind("Job")
).Criteria(item => {
if (item.config.metadata?.ownerReferences) {
return false;
}
return true;
})
Transform
Query results can be transformed into another object:
Transform(
Api('batch')
.Kind("CronJob")
).To(item => ({
synthetic: true,
apiVersion: 'v1',
kind: 'PodSpec',
metadata: {
...item.config.spec?.template?.metadata ?? {},
name: `CronJob-${item.config.metadata?.name}`
},
spec: item.config.spec?.jobTemplate?.spec?.template?.spec
}))
TransformMany
Query results can be transformed into multiple other objects:
TransformMany(
Shortcut("PodSpec")
).To((item) => {
const results = [];
for (const cont of item?.config.spec?.containers ?? []) {
results.push({
synthetic: true,
apiVersion: "v1",
kind: "ContainerSpec",
metadata: {
...(item.config.spec?.template?.metadata ?? {}),
name: `${item.config.metadata?.name}-Cont-${cont.name}`,
},
spec: cont,
});
}
for (const cont of item?.config.spec?.initContainers ?? []) {
results.push({
synthetic: true,
apiVersion: "v1",
kind: "ContainerSpec",
metadata: {
...(item.config.spec?.template?.metadata ?? {}),
name: `${item.config.metadata?.name}-InitCont-${cont.name}`,
},
spec: cont,
});
}
return results;
});
Rule Script
The rule script is executed for every item returned from the target script. The purpose of the rule script is to validate whether the item is good or not. In case of violations, it should trigger errors or warnings:
error(`Something is wrong with the object ${item.name}!`);
warning(`We prefer not configuring ${config.kind} in this way...`);
The global scope of the rule script contains:
| Variable | Description | | -------- | ------------------------------------------------------------ | | config | The raw Kubernetes object | | item | A wrapper around the config object. It provides quick access to:- item.name- item.namespace- item.labels- item.annotations | | helpers | Contains common utility functions. See the reference here | | cache | Scoped cache object. Learn more here |
Sample rule script:
if (helpers.parseImage(config.spec.image).tag === 'latest') {
error(`Latest Image Tags not allowed: ${config.spec.image}`);
}
Nested queries
Additional queries can be made from the rule script following the target script syntax and by adding .many(), .single(), or .count() suffix.
In the example below we can loop through HPAs:
for(const hpa of Api('autoscaling')
.Kind('HorizontalPodAutoscaler')
.many())
{
// do something
}
or query the ConfigMap by name:
const configMap = ApiVersion('v1')
.Kind('ConfigMap')
.name('mysql-configs')
.single();
if (!configMap) {
error('Could not find the ConfigMap');
}
Query Scope
Nested queryes by default are scoped to the namespace of the target item. To query objects from a specific namespace, the .namespace('...')
query filter should be used:
const gatewayItem = Api('gateway.networking.k8s.io')
.Kind('Gateway')
.name('my-gateway')
.namespace('from-another-namespace')
.single();
When querying Clustered objects the .isClusterScope(true)
filter should always be set:
Api('rbac.authorization.k8s.io')
.Kind('ClusterRole')
.isClusterScope(true)
It is also possible to query Namespaced objects globally by settings .allNamespaces()
filter should always be set:
Api('rbac.authorization.k8s.io')
.Kind('Role')
.allNamespaces()
Cache Script
Cache scripts are optional. They are called only once per Namespace or once per Cluster scope. Cache scripts share a global cache object which is unique per Namespace. It is useful to perform queries and building lookup queries.
Following cache scripts builds a Pod Label dictionary once per Namespace:
cache.apps = helpers.newLabelLookupDict();
for(const app of Shortcut("PodSpec")
.many())
{
cache.apps.add(app, app.config.metadata?.labels);
}
It is then accessible to items in the rule scripts:
const apps = cache.apps.resolveSelector(config.spec.selector)
if (apps.length === 0)
{
error(`Could not find Applications for Service`);
}
Here is a good example for cache script usage: https://github.com/kubevious/rules-library/blob/main/k8s/service/service-selector-ref.yaml
Global Helpers
Scripts have access to following helper functions:
Parsing image string into image and tag
parseImage(fullImage: string) : { image: string, tag: string };
Helper to perform label selector lookup
newLabelLookupDict() : {
add(item, labels),
resolveSelector(selector) : items[],
matchesSelector(selector) : boolean
}
Helper to perform name lookup
newNameLookupDict(items) : {
resolve(name) : Item | null,
contains(name) : boolean
}
Labels dictionary to string
labelsToString(labels) : string