...
1# Copyright 2023 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import git
16import yaml
17import os
18from deepdiff import DeepDiff
19
20# Truncate the field path returned by deepdiff.
21def process_field_path(path, root):
22 return path.replace("['", ".").replace("']", "").replace(".properties", "").replace("root", root)
23
24# Extract the kind name from the CRD file content.
25# Line #18 in the CRD file is usually (if not always) in the format of:
26# `singular: [KindName]`.
27def extract_kind_name(filename):
28 with open(filename, 'r') as fp:
29 row = fp.readlines()[17:18]
30 return row[0].split(": ")[1].rstrip()
31
32# has_description_changes_only returns true if there are description changes only.
33def has_description_changes_only(deep_diff_obj):
34 for item in deep_diff_obj.get('dictionary_item_added', []):
35 if "description" not in item:
36 return False
37 for item in deep_diff_obj.get('iterable_item_added', []):
38 if "description" not in item:
39 return False
40 for item in deep_diff_obj.get('dictionary_item_removed', []):
41 if "description" not in item:
42 return False
43 for item in deep_diff_obj.get('iterable_item_removed', []):
44 if "description" not in item:
45 return False
46 for key, _ in deep_diff_obj.get('values_changed', {}).items():
47 if "description" not in key:
48 return False
49 return True
50
51# print_diffs prints out diffs excluding description of CRDs in a readable format.
52def print_diffs(deep_diff_obj, root):
53 for item in deep_diff_obj.get('dictionary_item_added', []):
54 field_path = process_field_path(item, root)
55 if "description" not in field_path:
56 print_addition(field_path)
57 for item in deep_diff_obj.get('iterable_item_added', []):
58 field_path = process_field_path(item, root)
59 if "description" not in field_path:
60 print_addition(field_path)
61 for item in deep_diff_obj.get('dictionary_item_removed', []):
62 field_path = process_field_path(item, root)
63 if "description" not in field_path:
64 print_removal(field_path)
65 for item in deep_diff_obj.get('iterable_item_removed', []):
66 field_path = process_field_path(item, root)
67 if "description" not in field_path:
68 print_removal(field_path)
69 for key, value in deep_diff_obj.get('values_changed', {}).items():
70 field_path = process_field_path(key, root)
71 if "description" not in field_path:
72 print(" * Changed " + field_path + " from " + value['old_value'] + " to " + value['new_value'])
73
74def print_addition(field_path):
75 print(" * Added `" + field_path + "` field.")
76
77def print_removal(field_path):
78 print(" * Removed `" + field_path + "` field.")
79
80# Create a git Repo object.
81repo_path = '.'
82repo = git.Repo(repo_path)
83
84# Get the diff between the working tree and the last commit.
85diff = repo.head.commit.diff(None)
86latest_commit = repo.head.commit
87
88# Loop through the diff to find the changed YAML files under the config/crds/resources directory.
89for file_diff in diff:
90 if file_diff.a_path.startswith('config/crds/resources') and file_diff.a_path.endswith('.yaml'):
91 # Open the YAML file and parse its content.
92 file_path = os.path.join(repo_path, file_diff.a_path)
93 with open(file_path, 'r') as yaml_file:
94 new_yaml_data = yaml.safe_load(yaml_file)
95 new_spec = new_yaml_data["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["spec"]["properties"]
96 new_status = new_yaml_data["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["status"]["properties"]
97 # Read the YAML file from the latest commit and parse its content.
98 old_file_content = latest_commit.tree / file_diff.a_path
99 old_yaml_data = yaml.safe_load(old_file_content.data_stream.read())
100 old_spec = old_yaml_data["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["spec"]["properties"]
101 old_status = old_yaml_data["spec"]["versions"][0]["schema"]["openAPIV3Schema"]["properties"]["status"]["properties"]
102 # Compute the field differences between the old and new spec.
103 spec_differences = DeepDiff(old_spec, new_spec, ignore_order=True)
104 # Compute the field differences between the old and new status.
105 status_differences = DeepDiff(old_status, new_status, ignore_order=True)
106 # Print out the diffs in a readable format.
107 if has_description_changes_only(spec_differences) and has_description_changes_only(status_differences):
108 continue
109 version = old_yaml_data["spec"]["versions"][0]["name"]
110 print("* Resource " + extract_kind_name(file_path) +"(" + version + "):")
111 print_diffs(spec_differences, "spec")
112 print_diffs(status_differences, "status")
View as plain text