Repository: deis/deis

Issue:
check client version before sending API requests
I just spun up a new environment and get the following ( yes I have uploaded my keys )

```
$ deis create        
Creating application... done, created steely-bullfrog
git@deis's password: 
```

my .git/config has the following

```
[remote "deis"]
    url = git@deis:8000:steely-bullfrog.git
    fetch = +refs/heads/*:refs/remotes/deis/*
```

shouldn't it be port 2222 to match the builder container ?



Discussion:
ahhh, this turned out to be caused by running an old version of the deis CLI.   perhaps the server should refuse connection from older clients to avoid this kind of behavior ?

@paulczar agree we need to fix this by checking client version.

@mboersma i believe we have some of the infrastructure in place for this, what's the current status?

We do have a way to ask the server for its version and compare that to the client, but we don't yet do anything with that information. The simplest change would be to allow connecting but warn the user if her client is out of date.

If you are using 0.4.1 should the port be 8000?  My .git/config lists 2222.

@azurewraith The differences between 0.4.x and 0.5.x are unusually large. Do you have a particular reason for using 0.4.1?

I started with the current 'release' version but I think that was a bad idea...

Yes, 0.5 isn't the most stable release, you're right. There's been such huge changes in 0.5 though, it's likely that a lot of the problems you come across have been fixed or are no longer relevant. As instable as 0.5 I'd recommend struggling with that, it will pay off for you in the long term.

@tombh I'll give the master branch a shot then.  Sometimes it is fun to live on the edge...

![Difficulty Level: Bring 'Em On](http://blog.weflyspitfires.com/wp-content/uploads/2012/06/Wolfenstein-Difficulty.png)

There's currently no endpoint that I'm aware of on the server which dumps server information. What does everyone think of a URL like `/info` which would dump out some basic information about the controller, including the release and API version? This endpoint would be globally readable i.e. you do not require authentication to retrieve this information. You'd just cURL http://deis.local.deisapp.com/info/ and you'd get something like the following:

```
{"version": "v0.10.0", "api_version": "v1"}
```

We could then parse this information in the client in order to get relevant information like if we can run certain commands with the current CLI version. The API versioning stuff would be implemented in #1042, so that's a separate PR, but this would be a first step in that direction.

Thoughts/suggestions?


Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -362,11 +362,14 @@ def __init__(self):
         self._session = Session()
         self._settings = Settings()
 
-    def _dispatch(self, method, path, body=None,
-                  headers={'content-type': 'application/json'}, **kwargs):
+    def _dispatch(self, method, path, body=None, **kwargs):
         """
         Dispatch an API request to the active Deis controller
         """
+        headers = {
+            'content-type': 'application/json',
+            'X-Deis-Version': __version__.rsplit('.', 1)[0],
+        }
         func = getattr(self._session, method.lower())
         controller = self._settings['controller']
         if not controller:
@@ -374,6 +377,10 @@ def _dispatch(self, method, path, body=None,
                 'No active controller. Use `deis login` or `deis register` to get started.')
         url = urlparse.urljoin(controller, path, **kwargs)
         response = func(url, data=body, headers=headers)
+        # check for errors
+        if response.json().get('error') is not None:
+            print(response.json()['error'])
+            sys.exit(1)
         return response
 
     def apps(self, args):
@@ -1596,7 +1603,7 @@ def main():
     call the appropriate method on the client.
     """
     cli = DeisClient()
-    args = docopt(__doc__, version='Deis CLI {}'.format(__version__),
+    args = docopt(__doc__, version=__version__,
                   options_first=True)
     cmd = args['<command>']
     cmd, help_flag = parse_args(cmd)
diff --git a/controller/api/middleware.py b/controller/api/middleware.py
new file mode 100644
--- /dev/null
+++ b/controller/api/middleware.py
@@ -0,0 +1,28 @@
+import json
+
+from django.http import HttpResponse
+from rest_framework import status
+
+from deis import __version__
+
+
+class VersionMiddleware:
+
+    def process_request(self, request):
+        try:
+            # server and client version must match "x.y"
+            client_version = request.META['HTTP_X_DEIS_VERSION']
+            server_version = __version__.rsplit('.', 1)[0]
+            if client_version != server_version:
+                message = {
+                    'error': 'Client and server versions do not match.\n' +
+                    'Client version: {}\n'.format(client_version) +
+                    'Server version: {}'.format(server_version)
+                }
+                return HttpResponse(
+                    json.dumps(message),
+                    content_type='application/json',
+                    status=status.HTTP_405_METHOD_NOT_ALLOWED
+                )
+        except KeyError:
+            pass
diff --git a/controller/deis/settings.py b/controller/deis/settings.py
--- a/controller/deis/settings.py
+++ b/controller/deis/settings.py
@@ -108,6 +108,7 @@
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
+    'api.middleware.VersionMiddleware',
     # Uncomment the next line for simple clickjacking protection:
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 )


Test patch:
diff --git a/controller/api/tests/__init__.py b/controller/api/tests/__init__.py
--- a/controller/api/tests/__init__.py
+++ b/controller/api/tests/__init__.py
@@ -41,6 +41,7 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
             test_labels, extra_tests, **kwargs)
 
 
+from .test_api_middleware import *  # noqa
 from .test_app import *  # noqa
 from .test_auth import *  # noqa
 from .test_build import *  # noqa
diff --git a/controller/api/tests/test_api_middleware.py b/controller/api/tests/test_api_middleware.py
new file mode 100644
--- /dev/null
+++ b/controller/api/tests/test_api_middleware.py
@@ -0,0 +1,48 @@
+"""
+Unit tests for the Deis api app.
+
+Run the tests with "./manage.py test api"
+"""
+from __future__ import unicode_literals
+
+from django.test import TestCase
+
+from deis import __version__
+
+
+class APIMiddlewareTest(TestCase):
+
+    """Tests middleware.py's business logic"""
+
+    fixtures = ['tests.json']
+
+    def setUp(self):
+        self.assertTrue(
+            self.client.login(username='autotest', password='password'))
+
+    def test_x_deis_version_header_good(self):
+        """
+        Test that when the version header is sent, the request is accepted.
+        """
+        response = self.client.get(
+            '/api/apps',
+            HTTP_X_DEIS_VERSION=__version__.rsplit('.', 1)[0]
+        )
+        self.assertEqual(response.status_code, 200)
+
+    def test_x_deis_version_header_bad(self):
+        """
+        Test that when an improper version header is sent, the request is declined.
+        """
+        response = self.client.get(
+            '/api/apps',
+            HTTP_X_DEIS_VERSION='1234.5678'
+        )
+        self.assertEqual(response.status_code, 405)
+
+    def test_x_deis_version_header_not_present(self):
+        """
+        Test that when the version header is not present, the request is accepted.
+        """
+        response = self.client.get('/api/apps')
+        self.assertEqual(response.status_code, 200)