Skip to content

Commit c36fa9c

Browse files
authored
Merge pull request #140 from OpenRailAssociation/validate-config-schema
Add schema validation for app, org and team config files
2 parents 8019302 + 7efce09 commit c36fa9c

File tree

4 files changed

+352
-4
lines changed

4 files changed

+352
-4
lines changed

gh_org_mgr/_config.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,93 @@
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": {
79+
"oneOf": [{"type": "null"}, {"type": "array", "items": {"type": "string"}}]
80+
},
81+
"member": {
82+
"oneOf": [{"type": "null"}, {"type": "array", "items": {"type": "string"}}]
83+
},
84+
"parent": {"type": "string"},
85+
"repos": {
86+
"type": "object",
87+
"propertyNames": {"type": "string"},
88+
"additionalProperties": {
89+
"type": "string",
90+
"enum": ["pull", "triage", "push", "maintain", "admin"],
91+
},
92+
},
93+
},
94+
"additionalProperties": False,
95+
}
96+
},
97+
"additionalProperties": False,
98+
"required": [],
99+
}
100+
21101

22102
def _find_matching_files(directory: str, pattern: str, only_one: bool = False) -> list[str]:
23103
"""
@@ -85,6 +165,16 @@ def _read_config_file(file: str) -> dict:
85165
return config
86166

87167

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

96186
# Read and parse config files for app and org
97187
cfg_app = _read_config_file(cfg_app_files[0])
188+
_validate_config_schema(file=cfg_app_files[0], cfg=cfg_app, schema=APP_CONFIG_SCHEMA)
98189
cfg_org = _read_config_file(cfg_org_files[0])
190+
_validate_config_schema(file=cfg_org_files[0], cfg=cfg_org, schema=ORG_CONFIG_SCHEMA)
99191

100192
# For the teams config files, we parse and combine them as there may be multiple
101193
cfg_teams: dict[str, Any] = {}
102194
# For this, merge the resulting dicts of the previously read files, and the current file
103195
# Compare their keys (team names). They must not be defined multiple times!
104196
for cfg_team_file in cfg_teams_files:
105197
cfg = _read_config_file(cfg_team_file)
198+
_validate_config_schema(file=cfg_team_file, cfg=cfg, schema=TEAM_CONFIG_SCHEMA)
106199
if overlap := set(cfg_teams.keys()) & set(cfg.keys()):
107200
logging.critical(
108201
"The config file '%s' contains keys that are also defined in "

0 commit comments

Comments
 (0)