Skip to content
LinkState
Go back

Normalize Before Approval or You Review Noise

Introduction to Canonicalization

Definition and Purpose

Canonicalization deterministically transforms configuration artifacts—raw text files, structured data payloads, or target inventories—into a single, predictable representation. By stripping superficial differences (whitespace, ordering, comment placement, quoting style) that do not affect semantic meaning, only intent‑bearing changes remain visible during review. In a delivery pipeline, canonicalization bridges the intended state (operator’s goal in the source‑of‑truth repo) and the rendered state (output of templating/generation). When both sides are canonical, any remaining divergence reflects a functional change, not formatting noise.

Benefits of Canonicalization in Config Management

Pre-Canonicalization Preparation

Identifying Config Sources and Formats

Before canonicalization, inventory all configuration inputs:

  1. Source Files: Jinja2 templates, raw CLI snippets, NetYANG models, Ansible vars, Terraform .tf files, vendor‑specific XML/JSON.
  2. Tool Payloads: JSON bodies from RESTCONF/gNMI clients, Ansible playbook extra‑vars, Salt pillar data, Terraform plan outputs.
  3. Target Lists: CSV inventories, Ansible static inventories, NetBox exports, DNS‑derived host lists, dynamic discovery outputs (LLDP, CDP).

Classify each source by native format (YAML, JSON, INI, XML, plain text) and semantic domain (interface config, routing policy, ACL, BGP peer list, etc.) to select the appropriate normalizer.

Tool Payload and Target List Standardization

Payloads and inventories often contain optional fields, varying key casing, or embedded metadata (timestamps, user IDs). Standardization steps:

These steps guarantee that two payloads conveying the same functional intent yield identical byte‑for‑byte output after canonicalization.

Canonicalization Techniques

Config File Formatting and Normalization

FormatNormalizerTypical CommandWhat It Fixes
YAMLyq eval -P . (or prettier --parser yaml)yq eval -P . in.yaml > out.yamlKey ordering, quoting style, block vs flow scalars
JSONjq . (or prettier --parser json)jq . in.json > out.jsonWhitespace, trailing commas (if using jq -c for compact)
INIcrudini --get + custom sort`crudini —get in.ini ”sort > out.ini`
XMLxmllint --format --recoverxmllint --format --recover in.xml > out.xmlAttribute ordering, whitespace, CDN handling
Plain Text (CLI snippets)awk '{$1=$1};1' + sort -u`awk ’{$1=$1};1’ in.txtsort -u > out.txt`

The output of each normalizer is a canonical rendered state ready for comparison with the intended state stored in version control.

Payload and Target List Data Structure Standardization

For structured payloads (JSON/YAML), canonicalization proceeds in three phases:

  1. Parsing: Load the document into a language‑native structure (e.g., Python dict, Go map[string]interface{}).
  2. Recursive Sorting: Walk the structure, sorting dictionary keys and list elements where order is semantically irrelevant (e.g., ACL entries, BGP community lists). Preserve order for sequences where it matters (e.g., route‑map statements).
  3. Serialization: Emit the sorted structure with a deterministic serializer (e.g., json.dumps(..., sort_keys=True, separators=(',', ':')) in Python, or yq -o=json --sort-keys).

Target lists follow the same pattern: parse → deduplicate → sort → serialize.

Handling Comments and Metadata in Config Files

Comments are discarded during canonicalization because they do not affect device behavior. To retain audit‑relevant metadata:

This approach ensures reviewers see only functional changes while preserving traceability if needed.

Implementation and Tools

Existing Tools for Canonicalization

Custom Scripting for Specific Use Cases

When built‑in tools cannot capture domain‑specific semantics (e.g., ACL entry ordering matters only within a given ACL name), a small Python script suffices:

#!/usr/bin/env python3
import json, sys

def canonicalize(obj):
    if isinstance(obj, dict):
        # Sort keys recursively
        return {k: canonicalize(v) for k, v in sorted(obj.items())}
    elif isinstance(obj, list):
        # Preserve order only if list elements are dicts with a 'seq' key
        if all(isinstance(e, dict) and 'seq' in e for e in obj):
            return [canonicalize(e) for e in obj]
        # Otherwise sort list elements lexicographically by their JSON representation
        return sorted([canonicalize(e) for e in obj], key=json.dumps)
    else:
        return obj

def main():
    data = json.load(sys.stdin)
    canon = canonicalize(data)
    json.dump(canon, sys.stdout, sort_keys=True, separators=(',', ':'))

if __name__ == '__main__':
    main()

Use it in a CI job, for example:

terraform show -json plan.json | ./canonicalize.py > plan.canonical.json

Integration with Version Control Systems (VCS) for Automated Canonicalization

A typical Git‑based workflow:

  1. Pre‑commit Hook: Runs the appropriate normalizer on staged files; if the file changes, the hook amends the commit with the canonical version.
  2. CI Pipeline: On pull request generation, a job renders templates, runs canonicalization, and publishes canonical artifacts as build assets (e.g., artifacts/configs/).
  3. Review Step: The diff shown in the PR compares the canonical baseline (from main) against the canonical rendered state of the branch—ensuring only intent‑bearing changes appear.
  4. Post‑merge: A canonicalization job reapplies normalizers to the merged commit to guard against drift introduced by manual edits bypassing the hook.

This integration guarantees that the intended state in the repository is always canonical, eliminating a class of false‑positive drift.

Troubleshooting Canonicalization Issues

Common Pitfalls and Errors

Debugging Techniques

  1. Render‑Then‑Canonicalize Diff:
    jinja2 template.j2 vars.yml > rendered.raw
    yq eval -P . rendered.raw > rendered.canon
    diff -u intended.canon rendered.canon
  2. Intermediate Representation Dump:
    Insert a step that prints the parsed Python object (print(json.dumps(data, indent=2))) to verify sorting logic.
  3. Selective Normalization Toggle:
    Temporarily disable list sorting to see if a particular list is the source of divergence.
  4. Hash‑Based Verification:
    Compute SHA‑256 of both intended and rendered canonical files; a mismatch confirms a functional change.
  5. Log Metadata Stripping:
    Keep a copy of stripped comments/metadata (grep '^#' original > stripped.meta) to ensure nothing valuable was lost unintentionally.

Resolving Conflicts and Inconsistencies

Code and CLI Examples

YAML and JSON Canonicalization Using Command‑Line Tools

# YAML: sort keys, use block style, drop extraneous whitespace
yq eval -P . vars.yaml > vars.yaml.canonical

# JSON: compact, sorted keys
jq -S . payload.json > payload.json.canonical

# XML: format, recover malformed bits, sort attributes (via xslt)
xmllint --format --recover config.xml > config.xml.canonical

# INI: sort sections and keys
crudini --get config.ini '' | sort > config.ini.sections
while read sec; do
    echo "[$sec]"
    crudini --get config.ini "$sec" | sort
done < config.ini.sections > config.ini.canonical

Python Scripting for Custom Canonicalization Tasks

#!/usr/bin/env python3
import yaml, json, sys, pathlib

def canonical_yaml(text):
    data = yaml.safe_load(text)
    # Recursively sort dict keys; keep list order unless marked
    def norm(obj):
        if isinstance(obj, dict):
            return {k: norm(v) for k, v in sorted(obj.items())}
        if isinstance(obj, list):
            # Heuristic: if list items are dicts with an 'order' key, keep that order
            if all(isinstance(i, dict) and 'order' in i for i in obj):
                return [norm(i) for i in obj]
            return sorted([norm(i) for i in obj], key=json.dumps)
        return obj
    return yaml.dump(norm(data), default_flow_style=False)

if __name__ == '__main__':
    # Example usage: read YAML from stdin, write canonical YAML to stdout
    sys.stdout.write(canonical_yaml(sys.stdin.read()))

This script can be dropped into a CI job:

kubectl get -o yaml deployments | ./canonical_yaml.py > deployments.canonical.yaml

Share this post on:

Previous Post
Refusal behavior needs regression gates too
Next Post
How slow can policy CI get at scale