Repository: jupyterhub/jupyterhub

Issue:
Dockerspawner with Github Oauth breaks in 0.7.0 (works in 0.6.1)
I set dockerspawner with the oauth example provided in the repository, using the pre-built singleuser image (`docker pull jupyter/singleuser`), and everything works when using jupyterhub 0.6.1.  However, if I switch to 0.7.0, the following break:

* logging out - it just redirects back to `/hub/home#` without logging out
* shutting down a server, then trying to launch it again - can no longer connect to the docker image, with a 500 error: `Permission failure checking authorization, I may need to be restarted`

If I rm -f the docker container, it starts back up again fine, but has the same problem if I shut down my server or the jupyterhub server.


With debug mode logging, when logging out, this is the output:

```
[I 2016-12-16 16:34:58.647 JupyterHub login:19] User logged out: X@Y.com
[I 2016-12-16 16:34:58.653 JupyterHub log:100] 302 GET /hub/logout (X@Y.com@50.202.79.166) 5.50ms
[W 2016-12-16 16:34:58.734 JupyterHub pages:32] Disallowing redirect outside JupyterHub: ''
[I 2016-12-16 16:34:58.738 JupyterHub log:100] 302 GET /hub/ (@50.xxxxxx) 1.73ms
[I 2016-12-16 16:34:58.821 JupyterHub log:100] 302 GET /oauth_login (@50.xxxxxx) 1.95ms
[I 2016-12-16 16:34:58.899 JupyterHub google:35] redirect_uri: 'http://ec2-xxxxx:8000/hub/oauth_callback'
[I 2016-12-16 16:34:58.902 JupyterHub log:100] 302 GET /hub/oauth_login (@50.x)
```


Discussion:
Indeed, this is #901. We need a way to tell JupyterHub that it shouldn't cleanup API tokens when stopping servers. In the meantime, if you set `DockerSpawner.remove_containers = True`, stopping a server will cleanup the container, and the next start will get a new token and connect successfully.

Resolving patch:
diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py
--- a/jupyterhub/spawner.py
+++ b/jupyterhub/spawner.py
@@ -52,6 +52,17 @@ class Spawner(LoggingConfigurable):
     authenticator = Any()
     api_token = Unicode()
 
+    will_resume = Bool(False,
+        help="""Whether the Spawner will resume on next start
+
+
+        Default is False where each launch of the Spawner will be a new instance.
+        If True, an existing Spawner will resume instead of starting anew
+        (e.g. resuming a Docker container),
+        and API tokens in use when the Spawner stops will not be deleted.
+        """
+    )
+
     ip = Unicode('127.0.0.1',
         help="""
         The IP address (or hostname) the single-user server should listen on.
diff --git a/jupyterhub/user.py b/jupyterhub/user.py
--- a/jupyterhub/user.py
+++ b/jupyterhub/user.py
@@ -234,6 +234,12 @@ def spawn(self, options=None):
                 # prior to 0.7, spawners had to store this info in user.server themselves.
                 # Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
                 self.log.warning("DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7")
+            if spawner.api_token != api_token:
+                # Spawner re-used an API token, discard the unused api_token
+                orm_token = orm.APIToken.find(self.db, api_token)
+                if orm_token is not None:
+                    self.db.delete(orm_token)
+                    self.db.commit()
         except Exception as e:
             if isinstance(e, gen.TimeoutError):
                 self.log.warning("{user}'s server failed to start in {s} seconds, giving up".format(
@@ -313,10 +319,13 @@ def stop(self):
             if self.server:
                 # cleanup server entry from db
                 self.db.delete(self.server)
-            orm_token = orm.APIToken.find(self.db, api_token)
-            if orm_token:
-                self.db.delete(orm_token)
             self.server = None
+            if not spawner.will_resume:
+                # find and remove the API token if the spawner isn't
+                # going to re-use it next time
+                orm_token = orm.APIToken.find(self.db, api_token)
+                if orm_token:
+                    self.db.delete(orm_token)
             self.db.commit()
         finally:
             self.stop_pending = False


Test patch: