Skip to content

Commit 7e39e99

Browse files
authored
Merge pull request #962 from jrdnbradford/validate-config-yaml
Validate tljh specific config
2 parents 242dca4 + 5469e21 commit 7e39e99

File tree

4 files changed

+174
-18
lines changed

4 files changed

+174
-18
lines changed

.github/workflows/unit-test.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ jobs:
5353
with:
5454
python-version: "${{ matrix.python_version }}"
5555

56-
- name: Install venv, git and setup venv
56+
- name: Install venv, git, pip and setup venv
5757
run: |
5858
export DEBIAN_FRONTEND=noninteractive
5959
apt-get update
6060
apt-get install --yes \
6161
python3-venv \
62+
python3-pip \
6263
bzip2 \
6364
git
6465

integration-tests/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Systemd inside a Docker container, for CI only
2-
ARG BASE_IMAGE=ubuntu:20.04
2+
ARG BASE_IMAGE=ubuntu:22.04
33
FROM $BASE_IMAGE
44

55
# DEBIAN_FRONTEND is set to avoid being asked for input and hang during build:
@@ -29,8 +29,8 @@ RUN systemctl set-default multi-user.target
2929
STOPSIGNAL SIGRTMIN+3
3030

3131
# Uncomment these lines for a development install
32-
#ENV TLJH_BOOTSTRAP_DEV=yes
33-
#ENV TLJH_BOOTSTRAP_PIP_SPEC=/srv/src
34-
#ENV PATH=/opt/tljh/hub/bin:${PATH}
32+
# ENV TLJH_BOOTSTRAP_DEV=yes
33+
# ENV TLJH_BOOTSTRAP_PIP_SPEC=/srv/src
34+
# ENV PATH=/opt/tljh/hub/bin:${PATH}
3535

3636
CMD ["/bin/bash", "-c", "exec /lib/systemd/systemd --log-target=journal 3>&1"]

tljh/config.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ def remove_item_from_config(config, property_path, value):
154154
return config_copy
155155

156156

157+
def validate_config(config, validate):
158+
"""
159+
Validate changes to the config with tljh-config against the schema
160+
"""
161+
import jsonschema
162+
163+
from .config_schema import config_schema
164+
165+
try:
166+
jsonschema.validate(instance=config, schema=config_schema)
167+
except jsonschema.exceptions.ValidationError as e:
168+
if validate:
169+
print(
170+
f"Config validation error: {e.message}.\n"
171+
"You can still apply this change without validation by re-running your command with the --no-validate flag.\n"
172+
"If you think this validation error is incorrect, please report it to https://github.com/jupyterhub/the-littlest-jupyterhub/issues."
173+
)
174+
exit()
175+
176+
157177
def show_config(config_path):
158178
"""
159179
Pretty print config from given config_path
@@ -167,73 +187,73 @@ def show_config(config_path):
167187
yaml.dump(config, sys.stdout)
168188

169189

170-
def set_config_value(config_path, key_path, value):
190+
def set_config_value(config_path, key_path, value, validate=True):
171191
"""
172192
Set key at key_path in config_path to value
173193
"""
174194
# FIXME: Have a file lock here
175-
# FIXME: Validate schema here
176195
try:
177196
with open(config_path) as f:
178197
config = yaml.load(f)
179198
except FileNotFoundError:
180199
config = {}
181-
182200
config = set_item_in_config(config, key_path, value)
183201

202+
validate_config(config, validate)
203+
184204
with open(config_path, "w") as f:
185205
yaml.dump(config, f)
186206

187207

188-
def unset_config_value(config_path, key_path):
208+
def unset_config_value(config_path, key_path, validate=True):
189209
"""
190210
Unset key at key_path in config_path
191211
"""
192212
# FIXME: Have a file lock here
193-
# FIXME: Validate schema here
194213
try:
195214
with open(config_path) as f:
196215
config = yaml.load(f)
197216
except FileNotFoundError:
198217
config = {}
199218

200219
config = unset_item_from_config(config, key_path)
220+
validate_config(config, validate)
201221

202222
with open(config_path, "w") as f:
203223
yaml.dump(config, f)
204224

205225

206-
def add_config_value(config_path, key_path, value):
226+
def add_config_value(config_path, key_path, value, validate=True):
207227
"""
208228
Add value to list at key_path
209229
"""
210230
# FIXME: Have a file lock here
211-
# FIXME: Validate schema here
212231
try:
213232
with open(config_path) as f:
214233
config = yaml.load(f)
215234
except FileNotFoundError:
216235
config = {}
217236

218237
config = add_item_to_config(config, key_path, value)
238+
validate_config(config, validate)
219239

220240
with open(config_path, "w") as f:
221241
yaml.dump(config, f)
222242

223243

224-
def remove_config_value(config_path, key_path, value):
244+
def remove_config_value(config_path, key_path, value, validate=True):
225245
"""
226246
Remove value from list at key_path
227247
"""
228248
# FIXME: Have a file lock here
229-
# FIXME: Validate schema here
230249
try:
231250
with open(config_path) as f:
232251
config = yaml.load(f)
233252
except FileNotFoundError:
234253
config = {}
235254

236255
config = remove_item_from_config(config, key_path, value)
256+
validate_config(config, validate)
237257

238258
with open(config_path, "w") as f:
239259
yaml.dump(config, f)
@@ -336,6 +356,18 @@ def main(argv=None):
336356
argparser.add_argument(
337357
"--config-path", default=CONFIG_FILE, help="Path to TLJH config.yaml file"
338358
)
359+
360+
argparser.add_argument(
361+
"--validate", action="store_true", help="Validate the TLJH config"
362+
)
363+
argparser.add_argument(
364+
"--no-validate",
365+
dest="validate",
366+
action="store_false",
367+
help="Do not validate the TLJH config",
368+
)
369+
argparser.set_defaults(validate=True)
370+
339371
subparsers = argparser.add_subparsers(dest="action")
340372

341373
show_parser = subparsers.add_parser("show", help="Show current configuration")
@@ -383,13 +415,19 @@ def main(argv=None):
383415
if args.action == "show":
384416
show_config(args.config_path)
385417
elif args.action == "set":
386-
set_config_value(args.config_path, args.key_path, parse_value(args.value))
418+
set_config_value(
419+
args.config_path, args.key_path, parse_value(args.value), args.validate
420+
)
387421
elif args.action == "unset":
388-
unset_config_value(args.config_path, args.key_path)
422+
unset_config_value(args.config_path, args.key_path, args.validate)
389423
elif args.action == "add-item":
390-
add_config_value(args.config_path, args.key_path, parse_value(args.value))
424+
add_config_value(
425+
args.config_path, args.key_path, parse_value(args.value), args.validate
426+
)
391427
elif args.action == "remove-item":
392-
remove_config_value(args.config_path, args.key_path, parse_value(args.value))
428+
remove_config_value(
429+
args.config_path, args.key_path, parse_value(args.value), args.validate
430+
)
393431
elif args.action == "reload":
394432
reload_component(args.component)
395433
else:

tljh/config_schema.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
The schema against which the TLJH config file can be validated.
3+
4+
Validation occurs when changing values with tljh-config.
5+
"""
6+
7+
config_schema = {
8+
"$schema": "http://json-schema.org/draft-07/schema#",
9+
"title": "Littlest JupyterHub YAML config file",
10+
"definitions": {
11+
"BaseURL": {
12+
"type": "string",
13+
},
14+
"Users": {
15+
"type": "object",
16+
"additionalProperties": False,
17+
"properties": {
18+
"extra_user_groups": {"type": "object", "items": {"type": "string"}},
19+
"allowed": {"type": "array", "items": {"type": "string"}},
20+
"banned": {"type": "array", "items": {"type": "string"}},
21+
"admin": {"type": "array", "items": {"type": "string"}},
22+
},
23+
},
24+
"Services": {
25+
"type": "object",
26+
"properties": {
27+
"cull": {
28+
"type": "object",
29+
"additionalProperties": False,
30+
"properties": {
31+
"enabled": {"type": "boolean"},
32+
"timeout": {"type": "integer"},
33+
"every": {"type": "integer"},
34+
"concurrency": {"type": "integer"},
35+
"users": {"type": "boolean"},
36+
"max_age": {"type": "integer"},
37+
"remove_named_servers": {"type": "boolean"},
38+
},
39+
}
40+
},
41+
},
42+
"HTTP": {
43+
"type": "object",
44+
"additionalProperties": False,
45+
"properties": {
46+
"address": {"type": "string", "format": "ipv4"},
47+
"port": {"type": "integer"},
48+
},
49+
},
50+
"HTTPS": {
51+
"type": "object",
52+
"additionalProperties": False,
53+
"properties": {
54+
"enabled": {"type": "boolean"},
55+
"address": {"type": "string", "format": "ipv4"},
56+
"port": {"type": "integer"},
57+
"tls": {"$ref": "#/definitions/TLS"},
58+
"letsencrypt": {"$ref": "#/definitions/LetsEncrypt"},
59+
},
60+
},
61+
"LetsEncrypt": {
62+
"type": "object",
63+
"additionalProperties": False,
64+
"properties": {
65+
"email": {"type": "string", "format": "email"},
66+
"domains": {
67+
"type": "array",
68+
"items": {"type": "string", "format": "hostname"},
69+
},
70+
"staging": {"type": "boolean"},
71+
},
72+
},
73+
"TLS": {
74+
"type": "object",
75+
"additionalProperties": False,
76+
"properties": {"key": {"type": "string"}, "cert": {"type": "string"}},
77+
},
78+
"Limits": {
79+
"description": "User CPU and memory limits.",
80+
"type": "object",
81+
"additionalProperties": False,
82+
"properties": {"memory": {"type": "string"}, "cpu": {"type": "integer"}},
83+
},
84+
"UserEnvironment": {
85+
"type": "object",
86+
"additionalProperties": False,
87+
"properties": {
88+
"default_app": {
89+
"type": "string",
90+
"enum": ["jupyterlab", "classic"],
91+
"default": "jupyterlab",
92+
}
93+
},
94+
},
95+
"TraefikAPI": {
96+
"type": "object",
97+
"additionalProperties": False,
98+
"properties": {
99+
"ip": {"type": "string", "format": "ipv4"},
100+
"port": {"type": "integer"},
101+
"username": {"type": "string"},
102+
"password": {"type": "string"},
103+
},
104+
},
105+
},
106+
"properties": {
107+
"additionalProperties": False,
108+
"base_url": {"$ref": "#/definitions/BaseURL"},
109+
"user_environment": {"$ref": "#/definitions/UserEnvironment"},
110+
"users": {"$ref": "#/definitions/Users"},
111+
"limits": {"$ref": "#/definitions/Limits"},
112+
"https": {"$ref": "#/definitions/HTTPS"},
113+
"http": {"$ref": "#/definitions/HTTP"},
114+
"traefik_api": {"$ref": "#/definitions/TraefikAPI"},
115+
"services": {"$ref": "#/definitions/Services"},
116+
},
117+
}

0 commit comments

Comments
 (0)