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))