Created
November 12, 2025 20:04
-
-
Save timmc-edx/b0d841bbdff40feb92ea8d0e223c2c58 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| from pathlib import Path | |
| import yaml | |
| ALL_APPS_DIR = Path('argocd/applications') | |
| def descend_dict(d, ks): | |
| """ | |
| Recursively descend dictionary `d` by list of keys `ks`. | |
| Computes `d[k_1][k_2]...[kn]` unless any intermediate value (starting with d) | |
| is not a dict, in which case returns None. | |
| """ | |
| cur = d | |
| for k in ks: | |
| if not isinstance(cur, dict): | |
| return None | |
| cur = cur.get(k) | |
| return cur | |
| def check_file(yaml_path, app_dir): | |
| try: | |
| with open(yaml_path, 'r') as yp: | |
| yaml_docs = list(yaml.safe_load_all(yp.read())) | |
| except BaseException as e: | |
| # Some of the YAML files are actually templates and can't be parsed. But | |
| # at the time of this writing, none of them were `kind: Application` | |
| # anyhow. | |
| print(f"Warning: Unable to load YAML for {yaml_path}") | |
| return [] | |
| error_msgs = [] | |
| for (i, doc) in enumerate(yaml_docs): | |
| if not isinstance(doc, dict): | |
| continue # might be list, none, etc. | |
| if doc.get('kind') != 'Application': | |
| continue | |
| # We only want to lint applications defined in edx-internal. Some | |
| # files also don't have a repo URL because they're a kustomization | |
| # patch, so we'll assume that's the case when the URL is missing. | |
| repo_url = descend_dict(doc, ['spec', 'source', 'repoURL']) | |
| if repo_url != None and repo_url != '[email protected]:edx/edx-internal.git': | |
| print(f"Info: Skipping application from external repo {repo_url}: {yaml_path} #{i}") | |
| continue | |
| # Get ahold of the annotation, if it exists | |
| mgp = descend_dict(doc, ['metadata', 'annotations', 'argocd.argoproj.io/manifest-generate-paths']) | |
| if not isinstance(mgp, str) or mgp == '': | |
| error_msgs.append( | |
| f'Doc #{i} is an Application missing ' | |
| '.metadata.annotations["argocd.argoproj.io/manifest-generate-paths"]' | |
| ) | |
| continue | |
| if ';' in mgp: | |
| error_msgs.append( | |
| f"Doc #{i} has a semicolon in manifest-generate-paths" | |
| "(not supported by this check script)" | |
| ) | |
| continue | |
| # The mgp value should be a path (relative to `.spec.source.path`) that | |
| # matches the app dir. | |
| source_path = descend_dict(doc, ['spec', 'source', 'path']) | |
| if not isinstance(source_path, str): | |
| print(f"Warning: Skipping application with repo URL but not source path: {yaml_path} #{i}") | |
| continue | |
| resolved_mgp = (Path(source_path) / mgp).resolve() | |
| if not resolved_mgp.samefile(app_dir): | |
| error_msgs.append( | |
| f"Mismatched doc#{i}: {source_path=} + manifest-generate-paths={mgp} " | |
| f"did not match application dir {app_dir}" | |
| ) | |
| return error_msgs | |
| def check_application(app_dir): | |
| errors = [] | |
| for root, dirs, files in app_dir.walk(): | |
| for fname in files: | |
| if fname.lower().endswith(('.yml', '.yaml')): | |
| yaml_path = root / fname | |
| try: | |
| for error_msg in check_file(yaml_path, app_dir): | |
| errors.append((yaml_path, error_msg)) | |
| except BaseException as e: | |
| raise RuntimeError(f"Unexpected error while checking {yaml_path}") | |
| return errors | |
| def main(): | |
| errors = [] | |
| for app_entry in ALL_APPS_DIR.iterdir(): | |
| if app_entry.is_dir(): | |
| errors += check_application(app_entry) | |
| print('---') | |
| if errors: | |
| for (fpath, msg) in errors: | |
| print(f"Error in {fpath}: {msg}") | |
| sys.exit(1) | |
| else: | |
| print("No errors found") | |
| sys.exit(0) | |
| if __name__ == '__main__': | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Known problems: