Skip to content

Commit 2d702e3

Browse files
committed
feat: add a schema validation for app, org and team config files
1 parent 4855d34 commit 2d702e3

File tree

3 files changed

+347
-2
lines changed

3 files changed

+347
-2
lines changed

gh_org_mgr/_config.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,89 @@
1111
from typing import Any
1212

1313
import yaml
14+
from jsonschema import FormatChecker, validate
15+
from jsonschema.exceptions import ValidationError
1416

1517
# Global files with settings for the app and org, e.g. GitHub token and org name
1618
ORG_CONFIG_FILE = r"org\.ya?ml"
1719
APP_CONFIG_FILE = r"app\.ya?ml"
1820
TEAM_CONFIG_DIR = "teams"
1921
TEAM_CONFIG_FILES = r".+\.ya?ml"
2022

23+
# Schemas for config validation
24+
APP_CONFIG_SCHEMA = {
25+
"type": "object",
26+
"properties": {
27+
"github_token": {"type": "string"},
28+
"github_app_id": {"type": "integer"},
29+
"github_app_private_key": {"type": "string"},
30+
"remove_members_without_team": {"type": "boolean"},
31+
"delete_unconfigured_teams": {"type": "boolean"},
32+
},
33+
"additionalProperties": False,
34+
}
35+
ORG_CONFIG_SCHEMA = {
36+
"type": "object",
37+
"properties": {
38+
"org_name": {"type": "string"},
39+
"org_owners": {
40+
"type": "array",
41+
"items": {"type": "string"},
42+
"minItems": 1,
43+
},
44+
"defaults": {
45+
"type": "object",
46+
"properties": {
47+
"team": {
48+
"type": "object",
49+
"properties": {
50+
"description": {"type": "string"},
51+
"privacy": {"type": "string", "enum": ["secret", "closed"]},
52+
"notification_setting": {
53+
"type": "string",
54+
"enum": ["notifications_enabled", "notifications_disabled"],
55+
},
56+
},
57+
"additionalProperties": False,
58+
}
59+
},
60+
"additionalProperties": False,
61+
},
62+
},
63+
"additionalProperties": False,
64+
"required": ["org_name", "org_owners"],
65+
}
66+
TEAM_CONFIG_SCHEMA = {
67+
"type": "object",
68+
"patternProperties": {
69+
"^[a-zA-Z0-9 _\\-]+$": {
70+
"type": "object",
71+
"properties": {
72+
"description": {"type": "string"},
73+
"privacy": {"type": "string", "enum": ["secret", "closed"]},
74+
"notification_setting": {
75+
"type": "string",
76+
"enum": ["notifications_enabled", "notifications_disabled"],
77+
},
78+
"maintainer": {"type": "array", "items": {"type": "string"}},
79+
"member": {"type": "array", "items": {"type": "string"}},
80+
"parent": {"type": "string"},
81+
"repos": {
82+
"type": "object",
83+
"propertyNames": {"type": "string"},
84+
"additionalProperties": {
85+
"type": "string",
86+
"enum": ["pull", "triage", "push", "maintain", "admin"],
87+
},
88+
},
89+
},
90+
"additionalProperties": False,
91+
}
92+
},
93+
"additionalProperties": False,
94+
"required": [],
95+
}
96+
2197

2298
def _find_matching_files(directory: str, pattern: str, only_one: bool = False) -> list[str]:
2399
"""
@@ -85,6 +161,16 @@ def _read_config_file(file: str) -> dict:
85161
return config
86162

87163

164+
def _validate_config_schema(file: str, cfg: dict, schema: dict) -> None:
165+
"""Validate the config against a JSON schema"""
166+
try:
167+
validate(instance=cfg, schema=schema, format_checker=FormatChecker())
168+
except ValidationError as e:
169+
logging.critical("Config validation of file %s failed: %s", file, e.message)
170+
raise ValueError(e) from None
171+
logging.debug("Config in file %s validated successfully against schema.", file)
172+
173+
88174
def parse_config_files(path: str) -> tuple[dict[str, str | dict[str, str]], dict, dict]:
89175
"""Parse all relevant files in the configuration directory. Returns a tuple
90176
of org config, app config, and merged teams config"""
@@ -95,14 +181,17 @@ def parse_config_files(path: str) -> tuple[dict[str, str | dict[str, str]], dict
95181

96182
# Read and parse config files for app and org
97183
cfg_app = _read_config_file(cfg_app_files[0])
184+
_validate_config_schema(file=cfg_app_files[0], cfg=cfg_app, schema=APP_CONFIG_SCHEMA)
98185
cfg_org = _read_config_file(cfg_org_files[0])
186+
_validate_config_schema(file=cfg_org_files[0], cfg=cfg_org, schema=ORG_CONFIG_SCHEMA)
99187

100188
# For the teams config files, we parse and combine them as there may be multiple
101189
cfg_teams: dict[str, Any] = {}
102190
# For this, merge the resulting dicts of the previously read files, and the current file
103191
# Compare their keys (team names). They must not be defined multiple times!
104192
for cfg_team_file in cfg_teams_files:
105193
cfg = _read_config_file(cfg_team_file)
194+
_validate_config_schema(file=cfg_team_file, cfg=cfg, schema=TEAM_CONFIG_SCHEMA)
106195
if overlap := set(cfg_teams.keys()) & set(cfg.keys()):
107196
logging.critical(
108197
"The config file '%s' contains keys that are also defined in "

0 commit comments

Comments
 (0)