Repository: jupyterhub/jupyterhub

Issue:
Using --config with a missing config should raise an error
```shell
# this cause no error
jupyterhub --config /asdfasdf
```

I want the above to cause an error! If it doesn't, it cause various delayed errors. As an example in https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues/1463, a failure to find the passed --config led to a harder to debug error. In this case, the `jupyterhub/k8s-hub` image that doesn't ship with configurable-http-proxy, but it doesn't need it either assuming the config file is passed and found.

---

I inspected the current setup, and conclude that there is a default value on a `config_file` Traitlet set to `"jupyterhub_config.py"`, so it may not be obvious if it has been set explicitly or not which is required information for us.

One idea is to try to spot if it has been explicitly set or if we rely on the default value using traitlets know-how, and then run a check very early to see if it exists or not, and error out early if it doesn't. No matter what, we should catch a provided configuration file that doesn't exist somehow!

## Relevant parts of the code base

Where I think --config translates to the JupyterHub class traitlet config_file:
https://github.com/jupyterhub/jupyterhub/blob/703af1dd1eddb266e5fd80774e3baa413728be0c/jupyterhub/app.py#L100-L105

Where the config_file traitlet is defined along with a default value:
https://github.com/jupyterhub/jupyterhub/blob/703af1dd1eddb266e5fd80774e3baa413728be0c/jupyterhub/app.py#L310-L312

Where the config_file is first used:
https://github.com/jupyterhub/jupyterhub/blob/703af1dd1eddb266e5fd80774e3baa413728be0c/jupyterhub/app.py#L2207-L2213




Discussion:
@minrk I could benefit from a nudge on how to fix this.

Got any general advice or ideas about this?
Is it enough to catch this issue in the `initialize` function?
Do you know if I can detect if a traitlet has been explicitly set somehow?
In initialize should be fine.

> Do you know if I can detect if a traitlet has been explicitly set somehow?

Yes, `@validate` handlers are called whenever a trait is set (this is called prior to completing the attribute assignment, to allow validation, errors, or coercion). This would be the most traitlet-approved way to validate a trait. Additionally, `@observe` handlers are called *after* setting, and are usually used for linking one trait to another. Errors could be raised here, but it wouldn't prevent the assignment in the first place. That wouldn't really matter if we're calling `self.exit(1)` to exit the process, for instance.

Resolving patch:
diff --git a/jupyterhub/app.py b/jupyterhub/app.py
--- a/jupyterhub/app.py
+++ b/jupyterhub/app.py
@@ -57,6 +57,7 @@
     Float,
     observe,
     default,
+    validate,
 )
 from traitlets.config import Application, Configurable, catch_config_error
 
@@ -310,6 +311,19 @@ def _load_classes(self):
     config_file = Unicode('jupyterhub_config.py', help="The config file to load").tag(
         config=True
     )
+
+    @validate("config_file")
+    def _validate_config_file(self, proposal):
+        if not os.path.isfile(proposal.value):
+            print(
+                "ERROR: Failed to find specified config file: {}".format(
+                    proposal.value
+                ),
+                file=sys.stderr,
+            )
+            sys.exit(1)
+        return proposal.value
+
     generate_config = Bool(False, help="Generate default config file").tag(config=True)
     generate_certs = Bool(False, help="Generate certs used for internal ssl").tag(
         config=True


Test patch:
diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py
--- a/jupyterhub/tests/test_app.py
+++ b/jupyterhub/tests/test_app.py
@@ -3,9 +3,11 @@
 import os
 import re
 import sys
+import time
 from subprocess import check_output
 from subprocess import PIPE
 from subprocess import Popen
+from subprocess import run
 from tempfile import NamedTemporaryFile
 from tempfile import TemporaryDirectory
 from unittest.mock import patch
@@ -39,6 +41,24 @@ def test_token_app():
     assert re.match(r'^[a-z0-9]+$', out)
 
 
+def test_raise_error_on_missing_specified_config():
+    """
+    Using the -f or --config flag when starting JupyterHub should require the
+    file to be found and exit if it isn't.
+    """
+    process = Popen(
+        [sys.executable, '-m', 'jupyterhub', '--config', 'not-available.py']
+    )
+    # wait inpatiently for the process to exit
+    for i in range(100):
+        time.sleep(0.1)
+        returncode = process.poll()
+        if returncode is not None:
+            break
+    process.kill()
+    assert returncode == 1
+
+
 def test_generate_config():
     with NamedTemporaryFile(prefix='jupyterhub_config', suffix='.py') as tf:
         cfg_file = tf.name