Home

Awesome

Codefresh build status

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:

ShortcutDescription
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:

VariableDescription
configThe raw Kubernetes object
itemA wrapper around the config object. It provides quick access to:<br />- item.name<br />- item.namespace<br />- item.labels<br />- item.annotations
helpersContains common utility functions. See the reference here
cacheScoped 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