CUE lang and HCL map key validation

Context

I’m just starting to play with CUE lang to assess the benefit it can bring to the automation I’m doing. This is a short memo to remember how to do dictionary key validation with CUE lang and a little bit more.

My starting point was to do naming convention validation with CUE lang. I have the following data model in my Terraform files.

subnets = {
  "sub-primary"   = { "subnet" = ["10.0.0.0","10.0.1.0"], "mask"= 24, "router" = "rtr-primary"   },
  "sub-secondary" = { "subnet" = ["10.0.2.0"]           , "mask"= 24, "router" = "rtr-secondary" }
}

First I needed to transform my HCL data model into something Cue was able to understand so I choose to transform it in JSON. Only replacing = with : and adding double quotes around every string field do the trick.

{
  "subnets" : {
    "sub-primary"   : { "subnet" : ["10.0.0.0","10.0.1.0"], "mask" : 24, "router" : "rtr-primary"   },
    "sub-secondary" : { "subnet" : ["10.0.2.0"]           , "mask" : 24, "router" : "rtr-secondary" }
  }
}

Validation of the keys’s values.

We can write the simplest JSON file and CUE schema below to first see how to validate the name of the keys.

JSON file

{                         
  "subnets": {            
     "mytest1": "blabla1",
     "mytest2": "blabla2" 
  }                       
}                         

CUE Schema

#subnetstruct: {
	[string]: string 
}
subnets: #subnetstruct
  • subnets:
    Identifier that declares a variable named subnets. That identifier reference the #subnetstruct definition which defines a struct. If you then have a subnets variable in your JSON file, then it MUST match the struct defined otherwise the validation command will failed.
  • #subnetstruct
    This is the definition of our struct. The struct contains a pattern constraint.
  • [string]: string
    Is a pattern constraint, denoted [pattern]: value. In the example, The pattern MUST be a string and the value MUST also be a string.

The vet command validates data with schemas. We can test our schema with the following command. In case of success the command doesn’t reply anything.

cue vet simple.cue simple.json

If we replace strings with integer or list in the JSON file like below.

simpleFalse.json

{
  "maptest": {
     "mytest1": 1,
     "mytest2": ["mystring2","mystring21","mystring31"]
  }
}

You will have the following messages

noyel@goku-srv:~/script/cue$ cue vet simple.cue simpleFalse.json
maptest.mytest1: conflicting values 1 and string (mismatched types int and string):
    ./simple.cue:2:14
    ./simple.cue:5:10
    ./simpleFalse.json:3:17
maptest.mytest2: conflicting values string and ["mystring2","mystring21","mystring31"] (mismatched types string and list):
    ./simple.cue:2:14
    ./simpleFalse.json:4:17

One thing I want to point out is that if you do a typo in the Identifier and then it doesn’t match any variable in the JSON file, you will the same output than if your test are correct (nothing).

Overall validation

JSON file

{
  "subnets" : {
    "sub-primary"   : { "subnet" : ["10.0.0.0","10.0.1.0"], "mask" : 24, "router" : "rtr-primary"   },
    "sub-secondary" : { "subnet" : ["10.0.2.0"]           , "mask" : 24, "router" : "rtr-secondary" }
  }
}

CUE Schema

import ( "net" )
#subnetstruct: {
  [=~"^sub-"]: { "subnet" : [...net.IPv4], "mask" : int & >= 24 & <= 32, "router" : =~"^rtr-"} 
}
subnets: #subnetstruct

For the complete example, the identifier and struct are the same, the only difference is the pattern constraint.

  • The pattern of the pattern constraint is a regex. The string that matches the regex define the key of our map. The key MUST start with sub-
  • The value of the pattern constraint is a struct with 3 fields.
    • “subnet” : a list of strings that represent a valid IPv4 address. The three dots … represent an ellipsis, it allow to say “any number of IPv4”.
    • “mask” : an int greater or equal to 24 and lower or equal to 32.
    • “router” : a string that must start with rtr-

Referencing a definition should recursively close it. In theory it’s not possible to add more fields in the struct. In our case, the pattern constraint being a regex, you can add as many fields that matches the regex that you want. The values must conform the constraints you’ve put.

Bonus point, Chat GPT :D

I’ve asked Chat GPT to write a GO script that would transform my HCL into JSON. After multiple wrong responses and struggles, I’ve created a new session and it eventually found a working code.

Prompt

Could you write a GO code to transform the HCL below into JSON ?
        subnets = {
          "sub-primary"   = { "subnet" = ["10.0.0.0","10.0.1.0"], "mask"= 24, "router" = "rtr-primary"   },
          "sub-secondary" = { "subnet" = ["10.0.2.0"]           , "mask"= 24, "router" = "rtr-secondary" }
        }

Chat GPT GO code

package main

import (
	"encoding/json"
	"fmt"

	"github.com/hashicorp/hcl"
)

func main() {
	hclStr := `
        subnets = {
          "sub-primary"   = { "subnet" = ["10.0.0.0","10.0.1.0"], "mask"= 24, "router" = "rtr-primary"   },
          "sub-secondary" = { "subnet" = ["10.0.2.0"]           , "mask"= 24, "router" = "rtr-secondary" }
        }
    `
	var data map[string]interface{}
	err := hcl.Decode(&data, hclStr)
	if err != nil {
		fmt.Println(err)
		return
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonData))
}

Result on https://go.dev/play/

{
  "subnets": [
    {
      "sub-primary": [
        {
          "mask": 24,
          "router": "rtr-primary",
          "subnet": [
            "10.0.0.0",
            "10.0.1.0"
          ]
        }
      ],
      "sub-secondary": [
        {
          "mask": 24,
          "router": "rtr-secondary",
          "subnet": [
            "10.0.2.0"
          ]
        }
      ]
    }
  ]
}