Compare two YAML files structurally.
Build from source using the standard Rust cargo
build tool.
$ git clone https://github.com/robdavid/yamldiff.git
$ cd yamldiff
$ cargo install --path .
Given two YAML files.
original.yaml | modified.yaml |
---|---|
kind: ServiceAccount
metadata:
name: vault1-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault1
app.kubernetes.io/managed-by: Helm |
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault2-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault2
app.kubernetes.io/managed-by: Helm |
These files can be compared simply with:
yamldiff original.yaml modified.yaml
which will show the structural differences:
The files to be compared can consist of multiple documents.
original-mutlidoc.yaml | modified-multidoc.yaml |
---|---|
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault1-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault1
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault1
namespace: default
labels:
helm.sh/chart: vault-0.17.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault1
app.kubernetes.io/managed-by: Helm
|
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault2-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault2
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault2
namespace: default
labels:
helm.sh/chart: vault-0.17.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault2
app.kubernetes.io/managed-by: Helm
|
The documents are compared by matching them one-to-one positionally. The document index is shown in the difference output.
yamldiff original-multidoc.yaml modified-multidoc.yaml
If any of the documents in either file cannot be matched, for example if there is an unequal number of documents between the two files, the difference is shown as deletions or insertions in the output.
yamldiff original-multidoc.yaml modified.yaml
When comparing Kubernetes YAML files consisting of multiple documents, the documents can be matched by group, version, kind, name and namespace, rather than just position in the file, by specifying the -k
(or --k8s
) flag.
Consider the following two Kubernetes YAML files, which have their documents in opposite orders:
original.yaml | modified.yaml |
---|---|
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: default
labels:
helm.sh/chart: vault-0.17.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm |
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: default
labels:
helm.sh/chart: vault-0.17.1
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
|
A standard diff will compare the documents in the order they appear, producing apparently several differences.
yamldiff original.yaml modified.yaml
However adding the -k
flag will match documents by Kubernetes resource type, name and namespace, providing us with a more representative picture.
yamldiff -k original.yaml modified.yaml
Sometimes, in order to understand the differences between files, it is useful to be able to perform some transformations on the input files prior to comparison. For example, consider to the following two files.
vault1.yaml | vault2.yaml |
---|---|
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault1
namespace: default
labels:
helm.sh/chart: vault-0.17.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault1
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault1-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault1
app.kubernetes.io/managed-by: Helm |
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault2-agent-injector
namespace: default
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault2
app.kubernetes.io/managed-by: Helm
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault2
namespace: default
labels:
helm.sh/chart: vault-0.17.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault2
app.kubernetes.io/managed-by: Helm |
Comparing these files naively shows them to be quite different.
$ yamldiff --count vault1.yaml vault2.yaml
8 differences (additions: 1, removals: 1, changes: 6)
This is because the resource types don't match positionally. So lets add the --k8s
flag to try to match by resource type.
$ yamldiff --count --k8s vault1.yaml vault2.yaml
34 differences (additions: 17, removals: 17)
This is even worse! Virtually every property now appears to be different. This is because the resource documents can't be matched, since they are named differently ("vault1" naming as compared with "vault2).
It's possible to do some transforms on the inputs before they are matched. This allows you to work around such systemic differences to get a better picture by compensating for known differences and see what remains. The way to do this is to create a strategy file. Here is an example to deal with this case.
transform:
original:
- select:
- path: kind
regex: .+
replace:
- path: metadata.name
regex: vault1
with: vault2
This is a single strategy rule that is applying a selective transformation on documents in the the original file (the first file argument). The single transform rule is selecting any document that has a non-empty kind
property, and for those documents is modifying the property at the path metadata.name
, replacing any occurrence of the regular expression "vault1", with "vault2". For our example, this is sufficient to transform the resource naming in the original file to match that of the modified file (in the second argument).
Assuming this is saved in a file named strat.yaml
, you can instruct yamldiff
to apply it's rules by specifying a -f
(or --strategy
) option:
yamldiff --k8s -f strat.yaml vault1.yaml vault2.yaml
Now we are comparing like-for-like resources, and can more clearly see the differences.
The full spec of transformation rules has the following structure 1:
transform:
original: &transform_block
- select:
- path: dotted.path
value: match_value
- path: other.dotted.path
regex: match_expression
replace:
- path: dotted.path
regex: match_expression
with: substitution
- path: dotted.path
value: a_value
with: replacement_value
set:
- path: dotted.path
value: new_value
drop: false
modified: *transform_block
both: *transform_block
-
original
The rules to transform the original file (first non-option argument). Consists of a list of transform rules, all of which are applied to the file prior to comparison.select
A list of rules that select YAML documents for transformation. All the criteria must match for a document to be selected.path
A YAML property path, in dotted notation, of a property to be matched. If an individual property key contains a.
character, it can be surrounded by square brackets, e.g."metadata.labels.[app.kubernetes.io/name]"
.regex
A document is only selected if the property contains a match of this regular expression. To match against the entire property value, use regular expression^
and$
characters. Only a properties of type string can be matched with a regular expression.value
A document is only selected if the value of the property matches this value. The type of the value can be a string, integer, float or boolean.
replace
A list of replacement rules that will be applied to matching documents. They will be applied in the order that they appear.path
A YAML property path, in dotted notation, of a property to be modified. If an individual property key contains a.
character, it can be surrounded by square brackets, e.g."metadata.labels.[app.kubernetes.io/name]"
.regex
A regular expression of a substring in the property to be replaced. This is only available on string property types. All matching occurrences will be replaced.value
If the property has this value, its value is modified. The type can e string, integer, float or boolean.with
The replacement value. For a regular expression match, this must be a string. Capture groups can also be specified using the syntax for Rust regex replacement strings, such as$1
for the first capture group. For value replacement, the type can be a string, integer, float or boolean.
set
Unconditionally set a property to a value in selected documents.path
A YAML property path, in dotted notation, of the property to be modified. If an individual property key contains a.
character, it can be surrounded by square brackets, e.g."metadata.labels.[app.kubernetes.io/name]"
.value
The value to be set, which can be a string, integer, float or boolean.
drop
If drop is true, the entire document is deleted. Incompatible withreplace
orset
.
-
modified
The rules to transform the modified file (the second non-option argument). These have the same structure as for the original file. -
both
The rules to transform both input files. These have the same structure as for the original and modified file.
It is also possible to perform filtering on the input files prior to indexing and comparison. There are two kinds of filtering; document filters that can selectively remove entire documents and path filters that can remove specific properties based on their paths. All filters are applied to both input files.
In the previous example, we saw the transformed file comparison yielded two differences based on a property difference found in both documents. It's possible to filter out the mismatched property in order to reduce the differences to zero. To do this, we can add path based filter rule to the strategy file:
# New filter
filter:
path:
exclude:
- name: metadata.labels.[app.kubernetes.io/instance]
# Original transform
transform:
original:
- select:
- path: kind
regex: .+
replace:
- path: metadata.name
regex: vault1
with: vault2
This will then prune out the non-matching property in each document, giving a final successful comparison .
$ yamldiff -ckf strat-filter.yaml vault1.yaml vault2.yaml
0 differences
The full specification of filter rules has the following structure1:
filter:
document:
exclude: &document_filter_rules
- properties:
- path: dotted.path
value: match_value
- path: other.dotted.path
regex: match_expression
include: *document_filter_rules
path:
exclude: &path_filter_rules
- name: path_name
- regex: path_pattern
include: *path_filter_rules
filter
Describes a set of filters, all of which are applied to both input files prior to comparison.document
Describes a set of document filters. By default, all documents are included. If anyinclude
filters are given, a document must match at least one of these to be included. If anyexclude
filters are given, it must also not match any of these to be included.exclude
Contains a list of property to value comparison objects. All the comparisons must fail for a document for it to be included.properties
Compare by list of property criteria. All the criteria in the list must pass for the comparison to succeed.path
The path name of a property to compare.value
The criterion is true if the value of the property is equal to this value. The type ofvalue
can be string, integer, float or boolean.regex
The criterion is true if the value of the property contains this regular expression. The type of the property must be a string.
include
Contains a list of comparison objects with the same structure asexclude
. This list must either be empty, or must contain at least one comparison that matches for a document in order for it to be included.
path
Describes a set of path filters. Properties with paths that do not match these rules are pruned from all documents of both input files prior to indexing and comparison.exclude
Contains path comparison objects. All the path comparisons must fail for a path for it to be included.include
Contains a list of comparison objects with the same structure asexclude
. This list must either be empty, or must contain one or more comparisons that match for a path in order for it to be included.-
name
Match a path by exact name. Path names are build from keys at each level of nesting in the YAML document, joined by a.
character. If an individual property key contains a.
character, it will be surrounded by square brackets, e.g."metadata.labels.[app.kubernetes.io/name]"
-
regex
The path comparison succeeds if the path name contains this regular expression. As withname
the path name comprises key names joined with.
with square brackets inserted as required. Matching any of these characters will require escaping them in the regular expression, e.g.regex: ^metadata\.labels\.\[app\.kubernetes\.io/name\]$
-
USAGE:
yamldiff [OPTIONS] <FILE1> <FILE2>
ARGS:
<FILE1> Original YAML file
<FILE2> Modified YAML file
OPTIONS:
-c, --count Display the number of differences only, rather than the differences
themselves
-f, --strategy <STRATEGY> File name of strategy file
-h, --help Print help information
-k, --k8s Compare kubernetes yaml documents
-n, --no-colour Don't produce coloured output
-x, --exclude <EXCLUDE> Exclude YAML document paths matching regex
Footnotes
-
This example is making use of YAML anchors (&) and references (*) to reduce repetition. ↩ ↩2