Repository: jupyterhub/zero-to-jupyterhub-k8s

Issue:
Hub failing on start when using GoogleOAuthenticator with null hostedDomain
## Overview
When setting the `auth.type = google`, leaving the `auth.google.hostedDomain` field blank or with an empty string(`""`) generates an TraitError. This does not happen if the field has any text present, but this breaks if you are only trying to authenticate via google's primary domain as opposed to a hosted domain (username@school.edu). It fails on line 81 of:   https://github.com/jupyterhub/oauthenticator/blob/master/oauthenticator/google.py   
because Google OAuth does not return a value for the `'hd'`.
```python
        if self.hosted_domain:
            if not username.endswith('@'+self.hosted_domain) or \
                bodyjs['hd'] != self.hosted_domain:
                raise HTTPError(403,
                    "You are not signed in to your {} account.".format(
                        self.hosted_domain)
                )
            else:
                username = username.split('@')[0]
```

It seems z2jh users/admin may want to use GoogleOAuthenticator without specifying a hosted-domain. i.e. We don't want the above to run if we're using Google's primary domain to authenticate, therefore a `null` or `""` needs to be a valid value in `config.yaml` for `auth.google.hostedDomain`.

### Work Around:
Add `c.GoogleOAuthenticator.hosted_domain = ''` to the `hub.extraConfig` section in your `config.yaml`

## Contents of `config.yaml`
```yaml
hub:
  extraConfig: |
    c.GoogleOAuthenticator.hosted_domain = ''

auth:
  type: google
  whitelist:
    users:
      - <EMAIL>@gmail.com
  admin:
    users:
      - <EMAIL>@gmail.com
  google:
    clientId: "<CLIENT_ID>"
    clientSecret: "<CLIENT_SECRET>"
    callbackUrl: "https://<hostname>/hub/oauth_callback"
    hostedDomain: ""
    loginService: "Google"
```

## Error message
```python
[E 2018-04-24 15:31:16.917 JupyterHub app:1623]
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 1620, in launch_instance_async
        yield self.initialize(argv)
      File "/usr/lib/python3.6/types.py", line 184, in throw
        return self.__wrapped.throw(tp, *rest)
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 1377, in initialize
        yield self.init_users()
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 930, in init_users
        if self.authenticator.enable_auth_state:
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 556, in __get__
        return self.get(obj, cls)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 535, in get
        value = self._validate(obj, dynamic_default())
      File "/usr/local/lib/python3.6/dist-packages/jupyterhub/app.py", line 544, in _authenticator_default
        return self.authenticator_class(parent=self, db=self.db)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/config/configurable.py", line 84, in __init__
        self.config = config
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 585, in __set__
        self.set(obj, value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 574, in set
        obj._notify_trait(self.name, old_value, new_value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 1139, in _notify_trait
        type='change',
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 1176, in notify_change
        c(change)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 819, in compatible_observer
        return func(self, change)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/config/configurable.py", line 186, in _config_changed
        self._load_config(change.new, traits=traits, section_names=section_names)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/config/configurable.py", line 153, in _load_config
        setattr(self, name, deepcopy(config_value))
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 585, in __set__
        self.set(obj, value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 559, in set
        new_value = self._validate(obj, value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 591, in _validate
        value = self.validate(obj, value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 2054, in validate
        self.error(obj, value)
      File "/usr/local/lib/python3.6/dist-packages/traitlets/traitlets.py", line 625, in error
        raise TraitError(e)
    traitlets.traitlets.TraitError: The 'hosted_domain' trait of a GoogleOAuthenticator instance must be a unicode string, but a value of None <class 'NoneType'> was specified.
```


Discussion:
Thank you for reporting this @townsenddw, you have made it very easy for me to grasp the situation! I love it!

## Summary
Your `config.yaml` is interpreted in the config-map template like this...
https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/a123fc14913eb7a670ce09cc31fcdf784477e3e0/jupyterhub/templates/hub/configmap.yaml#L21

And that config map is then used by the jupyterhub's default config...
https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/a123fc14913eb7a670ce09cc31fcdf784477e3e0/images/hub/jupyterhub_config.py#L135

And you are describing that adding the following in the additional config...
```python
c.GoogleOAuthenticator.hosted_domain = ''
```

Makes this issue no longer appear...
```
The 'hosted_domain' trait of a GoogleOAuthenticator instance must be a unicode string, but a value of None <class 'NoneType'> was specified.
```

## Preliminary analysis
`get_config('auth.google.hosted-domain')` returns a `None` type apparently, but we'd like for it to return a blank string or for the config to allow getting a `None` type value.

## TODO
I plan to do the following later
- [ ] Inspect how the generated configmap looks (`helm template ...`)
- [ ] Inspect how the `hosted_domain` file looks on a Pod consuming the configmap (access a running Hub pod)
This might be able to be handled by:
https://github.com/jupyterhub/zero-to-jupyterhub-k8s/commit/8f4e3c69df7a78f603eee02c493191d8e28507c1

@townsenddw I think you're right. The bug is in the jupyterhub_config.py, where it's blowing past the fact that a value is not set, and then *setting* an invalid None value as a result. Avoiding setting the value in the first place in the config is the right behavior.

Resolving patch:
diff --git a/images/hub/jupyterhub_config.py b/images/hub/jupyterhub_config.py
--- a/images/hub/jupyterhub_config.py
+++ b/images/hub/jupyterhub_config.py
@@ -132,7 +132,7 @@
     c.GoogleOAuthenticator.client_id = get_config('auth.google.client-id')
     c.GoogleOAuthenticator.client_secret = get_config('auth.google.client-secret')
     c.GoogleOAuthenticator.oauth_callback_url = get_config('auth.google.callback-url')
-    c.GoogleOAuthenticator.hosted_domain = get_config('auth.google.hosted-domain')
+    set_config_if_not_none(c.GoogleOAuthenticator, 'hosted_domain', 'auth.google.hosted-domain')
     c.GoogleOAuthenticator.login_service = get_config('auth.google.login-service')
     email_domain = get_config('auth.google.hosted-domain')
 elif auth_type == 'github':


Test patch: