mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #962 from jrdnbradford/validate-config-yaml
Validate tljh specific config
This commit is contained in:
3
.github/workflows/unit-test.yaml
vendored
3
.github/workflows/unit-test.yaml
vendored
@@ -53,12 +53,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python_version }}"
|
python-version: "${{ matrix.python_version }}"
|
||||||
|
|
||||||
- name: Install venv, git and setup venv
|
- name: Install venv, git, pip and setup venv
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install --yes \
|
apt-get install --yes \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
|
python3-pip \
|
||||||
bzip2 \
|
bzip2 \
|
||||||
git
|
git
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Systemd inside a Docker container, for CI only
|
# Systemd inside a Docker container, for CI only
|
||||||
ARG BASE_IMAGE=ubuntu:20.04
|
ARG BASE_IMAGE=ubuntu:22.04
|
||||||
FROM $BASE_IMAGE
|
FROM $BASE_IMAGE
|
||||||
|
|
||||||
# DEBIAN_FRONTEND is set to avoid being asked for input and hang during build:
|
# DEBIAN_FRONTEND is set to avoid being asked for input and hang during build:
|
||||||
|
|||||||
@@ -154,6 +154,26 @@ def remove_item_from_config(config, property_path, value):
|
|||||||
return config_copy
|
return config_copy
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(config, validate):
|
||||||
|
"""
|
||||||
|
Validate changes to the config with tljh-config against the schema
|
||||||
|
"""
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
|
from .config_schema import config_schema
|
||||||
|
|
||||||
|
try:
|
||||||
|
jsonschema.validate(instance=config, schema=config_schema)
|
||||||
|
except jsonschema.exceptions.ValidationError as e:
|
||||||
|
if validate:
|
||||||
|
print(
|
||||||
|
f"Config validation error: {e.message}.\n"
|
||||||
|
"You can still apply this change without validation by re-running your command with the --no-validate flag.\n"
|
||||||
|
"If you think this validation error is incorrect, please report it to https://github.com/jupyterhub/the-littlest-jupyterhub/issues."
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
def show_config(config_path):
|
def show_config(config_path):
|
||||||
"""
|
"""
|
||||||
Pretty print config from given config_path
|
Pretty print config from given config_path
|
||||||
@@ -167,30 +187,29 @@ def show_config(config_path):
|
|||||||
yaml.dump(config, sys.stdout)
|
yaml.dump(config, sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
def set_config_value(config_path, key_path, value):
|
def set_config_value(config_path, key_path, value, validate=True):
|
||||||
"""
|
"""
|
||||||
Set key at key_path in config_path to value
|
Set key at key_path in config_path to value
|
||||||
"""
|
"""
|
||||||
# FIXME: Have a file lock here
|
# FIXME: Have a file lock here
|
||||||
# FIXME: Validate schema here
|
|
||||||
try:
|
try:
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
config = yaml.load(f)
|
config = yaml.load(f)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
config = set_item_in_config(config, key_path, value)
|
config = set_item_in_config(config, key_path, value)
|
||||||
|
|
||||||
|
validate_config(config, validate)
|
||||||
|
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def unset_config_value(config_path, key_path):
|
def unset_config_value(config_path, key_path, validate=True):
|
||||||
"""
|
"""
|
||||||
Unset key at key_path in config_path
|
Unset key at key_path in config_path
|
||||||
"""
|
"""
|
||||||
# FIXME: Have a file lock here
|
# FIXME: Have a file lock here
|
||||||
# FIXME: Validate schema here
|
|
||||||
try:
|
try:
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
config = yaml.load(f)
|
config = yaml.load(f)
|
||||||
@@ -198,17 +217,17 @@ def unset_config_value(config_path, key_path):
|
|||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
config = unset_item_from_config(config, key_path)
|
config = unset_item_from_config(config, key_path)
|
||||||
|
validate_config(config, validate)
|
||||||
|
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def add_config_value(config_path, key_path, value):
|
def add_config_value(config_path, key_path, value, validate=True):
|
||||||
"""
|
"""
|
||||||
Add value to list at key_path
|
Add value to list at key_path
|
||||||
"""
|
"""
|
||||||
# FIXME: Have a file lock here
|
# FIXME: Have a file lock here
|
||||||
# FIXME: Validate schema here
|
|
||||||
try:
|
try:
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
config = yaml.load(f)
|
config = yaml.load(f)
|
||||||
@@ -216,17 +235,17 @@ def add_config_value(config_path, key_path, value):
|
|||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
config = add_item_to_config(config, key_path, value)
|
config = add_item_to_config(config, key_path, value)
|
||||||
|
validate_config(config, validate)
|
||||||
|
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def remove_config_value(config_path, key_path, value):
|
def remove_config_value(config_path, key_path, value, validate=True):
|
||||||
"""
|
"""
|
||||||
Remove value from list at key_path
|
Remove value from list at key_path
|
||||||
"""
|
"""
|
||||||
# FIXME: Have a file lock here
|
# FIXME: Have a file lock here
|
||||||
# FIXME: Validate schema here
|
|
||||||
try:
|
try:
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
config = yaml.load(f)
|
config = yaml.load(f)
|
||||||
@@ -234,6 +253,7 @@ def remove_config_value(config_path, key_path, value):
|
|||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
config = remove_item_from_config(config, key_path, value)
|
config = remove_item_from_config(config, key_path, value)
|
||||||
|
validate_config(config, validate)
|
||||||
|
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
yaml.dump(config, f)
|
yaml.dump(config, f)
|
||||||
@@ -336,6 +356,18 @@ def main(argv=None):
|
|||||||
argparser.add_argument(
|
argparser.add_argument(
|
||||||
"--config-path", default=CONFIG_FILE, help="Path to TLJH config.yaml file"
|
"--config-path", default=CONFIG_FILE, help="Path to TLJH config.yaml file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
argparser.add_argument(
|
||||||
|
"--validate", action="store_true", help="Validate the TLJH config"
|
||||||
|
)
|
||||||
|
argparser.add_argument(
|
||||||
|
"--no-validate",
|
||||||
|
dest="validate",
|
||||||
|
action="store_false",
|
||||||
|
help="Do not validate the TLJH config",
|
||||||
|
)
|
||||||
|
argparser.set_defaults(validate=True)
|
||||||
|
|
||||||
subparsers = argparser.add_subparsers(dest="action")
|
subparsers = argparser.add_subparsers(dest="action")
|
||||||
|
|
||||||
show_parser = subparsers.add_parser("show", help="Show current configuration")
|
show_parser = subparsers.add_parser("show", help="Show current configuration")
|
||||||
@@ -383,13 +415,19 @@ def main(argv=None):
|
|||||||
if args.action == "show":
|
if args.action == "show":
|
||||||
show_config(args.config_path)
|
show_config(args.config_path)
|
||||||
elif args.action == "set":
|
elif args.action == "set":
|
||||||
set_config_value(args.config_path, args.key_path, parse_value(args.value))
|
set_config_value(
|
||||||
|
args.config_path, args.key_path, parse_value(args.value), args.validate
|
||||||
|
)
|
||||||
elif args.action == "unset":
|
elif args.action == "unset":
|
||||||
unset_config_value(args.config_path, args.key_path)
|
unset_config_value(args.config_path, args.key_path, args.validate)
|
||||||
elif args.action == "add-item":
|
elif args.action == "add-item":
|
||||||
add_config_value(args.config_path, args.key_path, parse_value(args.value))
|
add_config_value(
|
||||||
|
args.config_path, args.key_path, parse_value(args.value), args.validate
|
||||||
|
)
|
||||||
elif args.action == "remove-item":
|
elif args.action == "remove-item":
|
||||||
remove_config_value(args.config_path, args.key_path, parse_value(args.value))
|
remove_config_value(
|
||||||
|
args.config_path, args.key_path, parse_value(args.value), args.validate
|
||||||
|
)
|
||||||
elif args.action == "reload":
|
elif args.action == "reload":
|
||||||
reload_component(args.component)
|
reload_component(args.component)
|
||||||
else:
|
else:
|
||||||
|
|||||||
117
tljh/config_schema.py
Normal file
117
tljh/config_schema.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
The schema against which the TLJH config file can be validated.
|
||||||
|
|
||||||
|
Validation occurs when changing values with tljh-config.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config_schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Littlest JupyterHub YAML config file",
|
||||||
|
"definitions": {
|
||||||
|
"BaseURL": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"Users": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"extra_user_groups": {"type": "object", "items": {"type": "string"}},
|
||||||
|
"allowed": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"banned": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"admin": {"type": "array", "items": {"type": "string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Services": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cull": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"timeout": {"type": "integer"},
|
||||||
|
"every": {"type": "integer"},
|
||||||
|
"concurrency": {"type": "integer"},
|
||||||
|
"users": {"type": "boolean"},
|
||||||
|
"max_age": {"type": "integer"},
|
||||||
|
"remove_named_servers": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTP": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"address": {"type": "string", "format": "ipv4"},
|
||||||
|
"port": {"type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPS": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"address": {"type": "string", "format": "ipv4"},
|
||||||
|
"port": {"type": "integer"},
|
||||||
|
"tls": {"$ref": "#/definitions/TLS"},
|
||||||
|
"letsencrypt": {"$ref": "#/definitions/LetsEncrypt"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LetsEncrypt": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"email": {"type": "string", "format": "email"},
|
||||||
|
"domains": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "format": "hostname"},
|
||||||
|
},
|
||||||
|
"staging": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"TLS": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {"key": {"type": "string"}, "cert": {"type": "string"}},
|
||||||
|
},
|
||||||
|
"Limits": {
|
||||||
|
"description": "User CPU and memory limits.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {"memory": {"type": "string"}, "cpu": {"type": "integer"}},
|
||||||
|
},
|
||||||
|
"UserEnvironment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"default_app": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["jupyterlab", "classic"],
|
||||||
|
"default": "jupyterlab",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"TraefikAPI": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"ip": {"type": "string", "format": "ipv4"},
|
||||||
|
"port": {"type": "integer"},
|
||||||
|
"username": {"type": "string"},
|
||||||
|
"password": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"additionalProperties": False,
|
||||||
|
"base_url": {"$ref": "#/definitions/BaseURL"},
|
||||||
|
"user_environment": {"$ref": "#/definitions/UserEnvironment"},
|
||||||
|
"users": {"$ref": "#/definitions/Users"},
|
||||||
|
"limits": {"$ref": "#/definitions/Limits"},
|
||||||
|
"https": {"$ref": "#/definitions/HTTPS"},
|
||||||
|
"http": {"$ref": "#/definitions/HTTP"},
|
||||||
|
"traefik_api": {"$ref": "#/definitions/TraefikAPI"},
|
||||||
|
"services": {"$ref": "#/definitions/Services"},
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user