Skip to content

Commit

Permalink
Merge pull request #962 from jrdnbradford/validate-config-yaml
Browse files Browse the repository at this point in the history
Validate tljh specific config
  • Loading branch information
consideRatio committed Apr 4, 2024
2 parents 242dca4 + 5469e21 commit 7e39e99
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ jobs:
with:
python-version: "${{ matrix.python_version }}"

- name: Install venv, git and setup venv
- name: Install venv, git, pip and setup venv
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install --yes \
python3-venv \
python3-pip \
bzip2 \
git
Expand Down
8 changes: 4 additions & 4 deletions integration-tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Systemd inside a Docker container, for CI only
ARG BASE_IMAGE=ubuntu:20.04
ARG BASE_IMAGE=ubuntu:22.04
FROM $BASE_IMAGE

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

# Uncomment these lines for a development install
#ENV TLJH_BOOTSTRAP_DEV=yes
#ENV TLJH_BOOTSTRAP_PIP_SPEC=/srv/src
#ENV PATH=/opt/tljh/hub/bin:${PATH}
# ENV TLJH_BOOTSTRAP_DEV=yes
# ENV TLJH_BOOTSTRAP_PIP_SPEC=/srv/src
# ENV PATH=/opt/tljh/hub/bin:${PATH}

CMD ["/bin/bash", "-c", "exec /lib/systemd/systemd --log-target=journal 3>&1"]
64 changes: 51 additions & 13 deletions tljh/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ def remove_item_from_config(config, property_path, value):
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):
"""
Pretty print config from given config_path
Expand All @@ -167,73 +187,73 @@ def show_config(config_path):
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
"""
# FIXME: Have a file lock here
# FIXME: Validate schema here
try:
with open(config_path) as f:
config = yaml.load(f)
except FileNotFoundError:
config = {}

config = set_item_in_config(config, key_path, value)

validate_config(config, validate)

with open(config_path, "w") as 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
"""
# FIXME: Have a file lock here
# FIXME: Validate schema here
try:
with open(config_path) as f:
config = yaml.load(f)
except FileNotFoundError:
config = {}

config = unset_item_from_config(config, key_path)
validate_config(config, validate)

with open(config_path, "w") as 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
"""
# FIXME: Have a file lock here
# FIXME: Validate schema here
try:
with open(config_path) as f:
config = yaml.load(f)
except FileNotFoundError:
config = {}

config = add_item_to_config(config, key_path, value)
validate_config(config, validate)

with open(config_path, "w") as 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
"""
# FIXME: Have a file lock here
# FIXME: Validate schema here
try:
with open(config_path) as f:
config = yaml.load(f)
except FileNotFoundError:
config = {}

config = remove_item_from_config(config, key_path, value)
validate_config(config, validate)

with open(config_path, "w") as f:
yaml.dump(config, f)
Expand Down Expand Up @@ -336,6 +356,18 @@ def main(argv=None):
argparser.add_argument(
"--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")

show_parser = subparsers.add_parser("show", help="Show current configuration")
Expand Down Expand Up @@ -383,13 +415,19 @@ def main(argv=None):
if args.action == "show":
show_config(args.config_path)
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":
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":
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":
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":
reload_component(args.component)
else:
Expand Down
117 changes: 117 additions & 0 deletions tljh/config_schema.py
Original file line number Diff line number Diff line change
@@ -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"},
},
}

0 comments on commit 7e39e99

Please sign in to comment.