Validate Configuration



Validating existing configuration is one of the easiest ways to get started with Cue. It is low impact and low risk as you are not changing anything you use today. When we validate existing configuration, we are designing a schema and using Cue to test against it.

A Simple Example

Let’s start out with a simple example for an album object. Here we see the JSON and Cue representation for an album.

album.json

{
  "album": {
    "artist": "Led Zeppelin",
    "title": "BBC Sessions",
    "date": "1997-11-11"
  }
}

album.cue

import "time"

#Album: {
	artist: string
	title:  string
	date:   string
	date:   time.Format("2006-01-02")
}

album: #Album

We start by defining a Cue #Album definition or schema which has the same structure. Instead of setting the fields to data, we set them to types string. Looking at the date field, we can see it repeated twice. This enables us to accumulate constraints across locations in a file or even across files and packages (how easier does applying company policies become?). Defining constraints on multiple lines is equivalent to using the conjunction operator (&) and would look like date: string & time.Format(...). We add an additional constraint using Cue’s standard library. We import "time" and use the time.Format("format") function to enforce the syntax for dates. The format string used is the same as Go’s. You can learn more about how Go time formats work in the Go docs for time. An important note to make about time formats is that the values used in the format template string are very specific to Go’s birth date/time. (Mon Jan 2 15:04:05 MST 2006) You can find more helpers in Cue’s standard library, just note that you cannot use the tool/... in normal Cue files. They are reserved for the scripting layer we will cover in a few sections.

With our schema #Album defined, we then assign it to an album field. This is required in our setup because of the way we shaped our data and the way Cue vet works. We’ll see other styles as we continue.

Now we can validate our album JSON file.

cue vet album.json album-schema.cue

You should see no output. Let’s see what an error looks like. If we change the album year to a two-digit representation 97 and run the same command, you should see the following error message:

$ cue vet album-lhs.json album-validate.cue 
error in call to time.Format: invalid time "97-11-11"

A Kubernetes Example

Let’s validate the Kubernetes manifests for this website. Below is the Yaml as a single file, you can find the originals in the repo.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
spec:
  selector:
    matchLabels:
      app: cuetorials

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  minReadySeconds: 5

  template:
    metadata:
      labels:
        app: cuetorials
    spec:
      containers:
      - name: website
        image: us.gcr.io/hof-io--develop/cuetorials.com:manual
        imagePullPolicy: Always
        ports:
        - containerPort: 80
          protocol: "TCP"

        readinessProbe:
          httpGet:
            port: 80
          initialDelaySeconds: 6
          failureThreshold: 3
          periodSeconds: 10
        livenessProbe:
          httpGet:
            port: 80
          initialDelaySeconds: 6
          failureThreshold: 3
          periodSeconds: 10

---
apiVersion: v1
kind: Service
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
spec:
  selector:
    app: cuetorials
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: cuetorials
  namespace: websites
  labels:
    app: cuetorials
  annotations:
    kubernetes.io/tls-acme: "true"
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod

    kubernetes.io/configuration-snippet: |
      location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
        expires 1h;
        access_log off;
        add_header Cache-Control "public";
      }
      location / {
        expires -1;
      }      

spec:
  tls:
  - hosts:
    - cuetorials.com
    secretName: cuetorials-tls

  rules:
  - host: cuetorials.com
    http:
      paths:
        - backend:
            serviceName: cuetorials
            servicePort: 80

Schema v1

As a first step, we setup some top-level definitions for our Kubernetes resources. We have a definition for each resource type and a #Schema which uses the disjunction operator (|) to say the schema must unify with one of these entries, like an “or” statement.

#Schema: #Deployment | #Service | #Ingress

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	...
}

Each resource type is defined as a struct ({}), defines fields for apiVersions and kind, and leaves the definition open with the ... (meaning more fields are allowed). By default definitions are closed and any undefined fields will cause an error. The apiVersion and kind fields are concrete values, meaning the configuration we validate must match exactly.

Let’s validate our Yaml with:

cue vet cuetorials.yaml cuetorials.cue -d "#Schema"

The extra -d "<path>" tells Cue the value we want to use to validate each Yaml entry against. We have to do this because Cue defaults to using top-level information, but we have three different schemas, one for each resource, and so we needed the disjunction and selector to make this work.


Schema v2

Now that we have some boilerplate, let’s define more schema to validate deeper into the objects.

#Schema: #Deployment | #Service | #Ingress

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {
		selector: {
			matchLabels: [string]: string
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {
		selector: [string]: string
		type: string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata: {
		name:       string
		namespace?: string
		labels: [string]:       string
		annotations?: [string]: string
	}
	spec: {...}
	...
}

Here are the things to pay attention to:

  • We’ve specified more, but not all of the schema. We still have the .... To prevent extraneous fields, you have to remove it.
  • The most general struct type is {...}. An empty struct looks like {}
  • labels: [string]: string is called a “Template” in Cue and defines labels as a struct with string fields with string values. You could define the value to be something more complex.
  • ports: [...{...}] is a list of structs. You can define a list of ints with ints: [...int]
  • namespace and annotations have a ? which makes them optional.

Schema v3

You’ll also notice in the last section that we had several schema sections which are repeated. We can reduce this by introducing some reusable definitions for label and metadata. The repeated sections are replaced with the definition label.

#Schema: #Deployment | #Service | #Ingress

#labels: [string]: string

#metadata: {
	name:       string
	namespace?: string
	labels:     #labels
	annotations?: [string]: string
}

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata:   #metadata
	spec: {
		selector: {
			matchLabels: #labels
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata:   #metadata
	spec: {
		selector: #labels
		type:     string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata:   #metadata
	spec: {...}
	...
}

Schema v4

Often, we want to ensure certain labels are specified and that they match within different sections of a resource. We change the schema to:

  • include labels: app: string to ensure an app label is on all resources. Notice that we can specify nested resources on a single line with path: to: nested: value.
  • use metadata.labels on #Deployment.spec.selector.matchLabels and #Service.spec.selector. This ensures that the labels are the same between the respective sections of the resources.
#Schema: #Deployment | #Service | #Ingress

#labels: [string]: string
#labels: app:      string

#metadata: {
	name:       string
	namespace?: string
	labels:     #labels
	annotations?: [string]: string
}

#Deployment: {
	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata:   #metadata
	spec: {
		selector: {
			matchLabels: metadata.labels
		}
		strategy: {...}
		minReadySeconds: uint
		template: {...}
	}
	...
}

#Service: {
	apiVersion: "v1"
	kind:       "Service"
	metadata:   #metadata
	spec: {
		selector: metadata.labels
		type:     string
		ports: [...{...}]
	}
	...
}

#Ingress: {
	apiVersion: "extensions/v1beta1"
	kind:       "Ingress"
	metadata:   #metadata
	spec: {...}
	...
}

Final schema

There is much more we could do to fill out this schema. We leave it as an exercise for the reader to add the details. In particular, the port schema and cross validation will reinforce what we showed here.

We'll never share your email with anyone else.
2023 Hofstadter, Inc