istio.io/istio@v0.0.0-20240520182934-d79c90f27776/bin/diff_yaml.py (about) 1 #!/usr/bin/env python 2 # 3 # Copyright 2018 Istio Authors 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 # Compare 2 multi document kubernetes yaml files 18 # It ensures that order does not matter 19 # 20 from __future__ import print_function 21 import argparse 22 import datadiff 23 import sys 24 import yaml # pyyaml 25 26 # returns fully qualified resource name of the k8s resource 27 28 29 def by_resource_name(res): 30 if res is None: 31 return "" 32 33 return "{}::{}::{}".format(res['apiVersion'], 34 res['kind'], 35 res['metadata']['name']) 36 37 38 def keydiff(k0, k1): 39 k0s = set(k0) 40 k1s = set(k1) 41 added = k1s - k0s 42 removed = k0s - k1s 43 common = k0s.intersection(k1s) 44 45 return added, removed, common 46 47 48 def drop_keys(res, k1, k2): 49 if k2 in res[k1]: 50 del res[k1][k2] 51 52 53 def normalize_configmap(res): 54 try: 55 if res['kind'] != "ConfigMap": 56 return res 57 58 data = res['data'] 59 60 # some times keys are yamls... 61 # so parse them 62 for k in data: 63 try: 64 op = yaml.safe_load_all(data[k]) 65 data[k] = list(op) 66 except yaml.YAMLError as ex: 67 print(ex) 68 69 return res 70 except KeyError as ke: 71 if 'kind' in str(ke) or 'data' in str(ke): 72 return res 73 74 raise 75 76 77 def normalize_ports(res): 78 try: 79 spec = res["spec"] 80 if spec is None: 81 return res 82 ports = sorted(spec['ports'], key=lambda x: x["port"]) 83 spec['ports'] = ports 84 85 return res 86 except KeyError as ke: 87 if 'spec' in str(ke) or 'ports' in str(ke) or 'port' in str(ke): 88 return res 89 90 raise 91 92 93 def normalize_res(res, args): 94 if not res: 95 return res 96 97 if args.ignore_labels: 98 drop_keys(res, "metadata", "labels") 99 100 if args.ignore_namespace: 101 drop_keys(res, "metadata", "namespace") 102 103 res = normalize_ports(res) 104 105 res = normalize_configmap(res) 106 107 return res 108 109 110 def normalize(rl, args): 111 for i in range(len(rl)): 112 rl[i] = normalize_res(rl[i], args) 113 114 return rl 115 116 117 def compare(args): 118 j0 = normalize(list(yaml.safe_load_all(open(args.orig))), args) 119 j1 = normalize(list(yaml.safe_load_all(open(args.new))), args) 120 121 q0 = {by_resource_name(res): res for res in j0 if res is not None} 122 q1 = {by_resource_name(res): res for res in j1 if res is not None} 123 124 added, removed, common = keydiff(q0.keys(), q1.keys()) 125 126 changed = 0 127 for k in sorted(common): 128 if q0[k] != q1[k]: 129 changed += 1 130 131 print("## +++ ", args.new) 132 print("## --- ", args.orig) 133 print("## Added:", len(added)) 134 print("## Removed:", len(removed)) 135 print("## Updated:", changed) 136 print("## Unchanged:", len(common) - changed) 137 138 for k in sorted(added): 139 print("+", k) 140 141 for k in sorted(removed): 142 print("-", k) 143 144 print("##", "*" * 25) 145 146 for k in sorted(common): 147 if q0[k] != q1[k]: 148 print("## ", k) 149 s0 = yaml.safe_dump(q0[k], default_flow_style=False, indent=2) 150 s1 = yaml.safe_dump(q1[k], default_flow_style=False, indent=2) 151 152 print(datadiff.diff(s0, s1, fromfile=args.orig, tofile=args.new)) 153 154 return changed + len(added) + len(removed) 155 156 157 def main(args): 158 return compare(args) 159 160 161 def get_parser(): 162 parser = argparse.ArgumentParser( 163 description="Compare kubernetes yaml files") 164 165 parser.add_argument("orig") 166 parser.add_argument("new") 167 parser.add_argument("--ignore-namespace", action="store_true", default=False, 168 help="Ignore namespace during comparison") 169 parser.add_argument("--ignore-labels", action="store_true", default=False, 170 help="Ignore resource labels during comparison") 171 parser.add_argument("--ignore-annotations", action="store_true", default=False, 172 help="Ignore annotations during comparison") 173 174 return parser 175 176 177 if __name__ == "__main__": 178 parser = get_parser() 179 args = parser.parse_args() 180 sys.exit(main(args))