Skip to content

A Worked Example

Everything so far, applied to one file: service.yml, the Beacon config from chapter 2. This chapter takes it apart the way you'd work on a real config — read it, convert it, ask it questions, and check it the way the program will.

The file, in sections

service:
  name: beacon
  version: 2.4.1
  environment: production

server:
  host: 0.0.0.0
  port: 8080
  workers: 4
  tls:
    enabled: true
    cert_file: /etc/beacon/cert.pem
    key_file: /etc/beacon/key.pem

Two mappings of plain scalars. Note the quiet decisions: version: 2.4.1 is a string because 2.4.1 is not a number (two dots) — chapter 8 showed what 2.4 would have risked. host: 0.0.0.0 is a string for the same reason. The booleans are written true/false, the spellings every parser shares.

checks:
  interval_seconds: 60
  timeout_seconds: 10
  retries: 2
  targets:
    - name: shop-front
      url: https://shop.example.com/
      expect_status: 200
    ...

The list of maps from chapter 4. The URLs are plain scalars that contain : but never : — https:// is safe unquoted, which is why the colon rule (chapter 8) so rarely bites URLs.

features:
  dashboard: true
  public_status_page: false
  beta_checks: off

And here the file walks into chapter 8 on purpose: beta_checks: off parses — under PyYAML — as the boolean False, which is what the author meant. show.py confirms all three:

features.dashboard                   True                         bool
features.public_status_page          False                        bool
features.beta_checks                   False                        bool

It works, and it's still the file's weakest line: under a YAML 1.2 parser off is the string "off", and a program testing truthiness may treat a non-empty string as enabled. false would say the same thing in every parser. (Chapter 10's linter flags this exact line.)

Convert it, then ask it questions

Because JSON is a subset of YAML, any YAML can be converted to JSON and handed to JSON tools. With yq:

$ yq -o=json '.' service.yml | head -20
{
  "service": {
    "name": "beacon",
    "version": "2.4.1",
    "environment": "production"
  },
  "server": {
    "host": "0.0.0.0",
    "port": 8080,
    "workers": 4,
    "tls": {
      "enabled": true,
      "cert_file": "/etc/beacon/cert.pem",
      "key_file": "/etc/beacon/key.pem"
    }
  },

(The head -20 trims it; the conversion itself is whole.) From there, jq — the subject of this series' previous guide — can interrogate it:

$ yq -o=json '.' service.yml | jq '.checks.targets[] | "\(.name) expects \(.expect_status)"'
"shop-front expects 200"
"api-health expects 200"
"blog expects 301"
$ yq -o=json '.' service.yml | jq '[.checks.targets[].expect_status] | unique'
[
  200,
  301
]

Conversion is also the cheapest sanity check there is: a file that converts has a structure, and the JSON view shows it without YAML's ambiguities — quotes on every string, types unmistakable.

Validate it like the program will

Parsing proves the file is YAML. It does not prove the config is right: a port of eighty is perfectly good YAML (a string) and a broken config. validate.py, shipped in examples/, checks the file the way the service would — required pieces present, each the type the service expects:

$ python3 validate.py service.yml; echo "[exit $?]"
ok   top level is a mapping
ok   service.name is a string (got 'beacon')
ok   service.version is a string (got str)
ok   server.port is a port number (got 8080)
ok   checks.targets is a non-empty list
ok   target shop-front: expect_status is an integer (got 200)
ok   target api-health: expect_status is an integer (got 200)
ok   target blog: expect_status is an integer (got 301)
ok   alerts.email is a list of strings
VALID
[exit 0]

Note service.version is a string among the checks: that line exists because of chapter 8. If an edit ever turns the version into a bare 2.4, the parser would hand over a float and this check — not the parser — is what would say so.

Now break one value in a scratch copy — port: 8080 becomes port: eighty:

$ sed 's/port: 8080/port: eighty/' service.yml > service-broken.yml
$ python3 validate.py service-broken.yml; echo "[exit $?]"
ok   top level is a mapping
ok   service.name is a string (got 'beacon')
ok   service.version is a string (got str)
FAIL server.port is a port number (got 'eighty')
ok   checks.targets is a non-empty list
ok   target shop-front: expect_status is an integer (got 200)
ok   target api-health: expect_status is an integer (got 200)
ok   target blog: expect_status is an integer (got 301)
ok   alerts.email is a list of strings
INVALID
[exit 1]

The parse never failed; the validation did, by name, with the offending value printed. That's the division of labour to aim for in your own tooling: YAML's parser for syntax, a checker for meaning, and a non-zero exit so a script — or a deployment pipeline — can tell the difference.

Check: the unmodified service.yml validates VALID with exit 0. Anything else means the file on disk isn't the file this chapter describes.