From ef8897f58bc959505be105c499d0bfbe5e872cd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= <fguillot@internap.com>
Date: Fri, 12 May 2017 13:28:10 -0400
Subject: [PATCH] Add API versioning

- All OpenStack projects have API versioning
- Existing endpoints are now prefixed with /v1
- Still fully backward compatible with old endpoints
- No HTTP redirects is used to avoid unexpected behaviors with
existing clients

Change-Id: If51f3291c44615991b3378b711dffacc1bd2591f
---
 almanach/api/v1/routes.py                     | 22 +++++-
 .../tests/tempest/services/almanach_client.py | 23 ++++---
 .../unit/api/v1/test_api_authentication.py    |  2 +-
 almanach/tests/unit/api/v1/test_api_entity.py | 26 ++++---
 almanach/tests/unit/api/v1/test_api_info.py   |  8 ++-
 .../tests/unit/api/v1/test_api_instance.py    | 68 ++++++++-----------
 almanach/tests/unit/api/v1/test_api_volume.py | 55 ++++++++++-----
 .../tests/unit/api/v1/test_api_volume_type.py | 10 +--
 doc/source/index.rst                          | 44 ++++++------
 9 files changed, 149 insertions(+), 109 deletions(-)

diff --git a/almanach/api/v1/routes.py b/almanach/api/v1/routes.py
index c73feff..4fa8c52 100644
--- a/almanach/api/v1/routes.py
+++ b/almanach/api/v1/routes.py
@@ -24,7 +24,7 @@ from werkzeug import wrappers
 from almanach.core import exception
 
 LOG = log.getLogger(__name__)
-api = flask.Blueprint("api", __name__)
+api = flask.Blueprint('v1', __name__)
 instance_ctl = None
 volume_ctl = None
 volume_type_ctl = None
@@ -85,12 +85,14 @@ def authenticated(api_call):
     return decorator
 
 
+@api.route("/v1/info", methods=["GET"])
 @api.route("/info", methods=["GET"])
 @to_json
 def get_info():
     return app_ctl.get_application_info()
 
 
+@api.route("/v1/project/<project_id>/instance", methods=["POST"])
 @api.route("/project/<project_id>/instance", methods=["POST"])
 @authenticated
 @to_json
@@ -112,6 +114,7 @@ def create_instance(project_id):
     return flask.Response(status=201)
 
 
+@api.route("/v1/instance/<instance_id>", methods=["DELETE"])
 @api.route("/instance/<instance_id>", methods=["DELETE"])
 @authenticated
 @to_json
@@ -126,6 +129,7 @@ def delete_instance(instance_id):
     return flask.Response(status=202)
 
 
+@api.route("/v1/instance/<instance_id>/resize", methods=["PUT"])
 @api.route("/instance/<instance_id>/resize", methods=["PUT"])
 @authenticated
 @to_json
@@ -141,6 +145,7 @@ def resize_instance(instance_id):
     return flask.Response(status=200)
 
 
+@api.route("/v1/instance/<instance_id>/rebuild", methods=["PUT"])
 @api.route("/instance/<instance_id>/rebuild", methods=["PUT"])
 @authenticated
 @to_json
@@ -158,6 +163,7 @@ def rebuild_instance(instance_id):
     return flask.Response(status=200)
 
 
+@api.route("/v1/project/<project_id>/instances", methods=["GET"])
 @api.route("/project/<project_id>/instances", methods=["GET"])
 @authenticated
 @to_json
@@ -167,6 +173,7 @@ def list_instances(project_id):
     return instance_ctl.list_instances(project_id, start, end)
 
 
+@api.route("/v1/project/<project_id>/volume", methods=["POST"])
 @api.route("/project/<project_id>/volume", methods=["POST"])
 @authenticated
 @to_json
@@ -186,6 +193,7 @@ def create_volume(project_id):
     return flask.Response(status=201)
 
 
+@api.route("/v1/volume/<volume_id>", methods=["DELETE"])
 @api.route("/volume/<volume_id>", methods=["DELETE"])
 @authenticated
 @to_json
@@ -200,6 +208,7 @@ def delete_volume(volume_id):
     return flask.Response(status=202)
 
 
+@api.route("/v1/volume/<volume_id>/resize", methods=["PUT"])
 @api.route("/volume/<volume_id>/resize", methods=["PUT"])
 @authenticated
 @to_json
@@ -215,6 +224,7 @@ def resize_volume(volume_id):
     return flask.Response(status=200)
 
 
+@api.route("/v1/volume/<volume_id>/attach", methods=["PUT"])
 @api.route("/volume/<volume_id>/attach", methods=["PUT"])
 @authenticated
 @to_json
@@ -230,6 +240,7 @@ def attach_volume(volume_id):
     return flask.Response(status=200)
 
 
+@api.route("/v1/volume/<volume_id>/detach", methods=["PUT"])
 @api.route("/volume/<volume_id>/detach", methods=["PUT"])
 @authenticated
 @to_json
@@ -245,6 +256,7 @@ def detach_volume(volume_id):
     return flask.Response(status=200)
 
 
+@api.route("/v1/project/<project_id>/volumes", methods=["GET"])
 @api.route("/project/<project_id>/volumes", methods=["GET"])
 @authenticated
 @to_json
@@ -254,6 +266,7 @@ def list_volumes(project_id):
     return volume_ctl.list_volumes(project_id, start, end)
 
 
+@api.route("/v1/project/<project_id>/entities", methods=["GET"])
 @api.route("/project/<project_id>/entities", methods=["GET"])
 @authenticated
 @to_json
@@ -263,6 +276,7 @@ def list_entity(project_id):
     return entity_ctl.list_entities(project_id, start, end)
 
 
+@api.route("/v1/entity/instance/<instance_id>", methods=["PUT"])
 @api.route("/entity/instance/<instance_id>", methods=["PUT"])
 @authenticated
 @to_json
@@ -277,6 +291,7 @@ def update_instance_entity(instance_id):
     return result
 
 
+@api.route("/v1/entity/<entity_id>", methods=["HEAD"])
 @api.route("/entity/<entity_id>", methods=["HEAD"])
 @authenticated
 def entity_exists(entity_id):
@@ -287,6 +302,7 @@ def entity_exists(entity_id):
     return response
 
 
+@api.route("/v1/entity/<entity_id>", methods=["GET"])
 @api.route("/entity/<entity_id>", methods=["GET"])
 @authenticated
 @to_json
@@ -294,6 +310,7 @@ def get_entity(entity_id):
     return entity_ctl.get_all_entities_by_id(entity_id)
 
 
+@api.route("/v1/volume_types", methods=["GET"])
 @api.route("/volume_types", methods=["GET"])
 @authenticated
 @to_json
@@ -302,6 +319,7 @@ def list_volume_types():
     return volume_type_ctl.list_volume_types()
 
 
+@api.route("/v1/volume_type/<volume_type_id>", methods=["GET"])
 @api.route("/volume_type/<volume_type_id>", methods=["GET"])
 @authenticated
 @to_json
@@ -310,6 +328,7 @@ def get_volume_type(volume_type_id):
     return volume_type_ctl.get_volume_type(volume_type_id)
 
 
+@api.route("/v1/volume_type", methods=["POST"])
 @api.route("/volume_type", methods=["POST"])
 @authenticated
 @to_json
@@ -323,6 +342,7 @@ def create_volume_type():
     return flask.Response(status=201)
 
 
+@api.route("/v1/volume_type/<volume_type_id>", methods=["DELETE"])
 @api.route("/volume_type/<volume_type_id>", methods=["DELETE"])
 @authenticated
 @to_json
diff --git a/almanach/tests/tempest/services/almanach_client.py b/almanach/tests/tempest/services/almanach_client.py
index 8349bad..b07ec8f 100644
--- a/almanach/tests/tempest/services/almanach_client.py
+++ b/almanach/tests/tempest/services/almanach_client.py
@@ -19,6 +19,7 @@ CONF = config.CONF
 
 
 class AlmanachClient(rest_client.RestClient):
+    api_version = 'v1'
 
     def __init__(self, auth_provider):
         super(AlmanachClient, self).__init__(
@@ -32,11 +33,11 @@ class AlmanachClient(rest_client.RestClient):
         return resp, response_body
 
     def create_server(self, tenant_id, body):
-        resp, response_body = self.post('/project/{}/instance'.format(tenant_id), body=body)
+        resp, response_body = self.post('project/{}/instance'.format(tenant_id), body=body)
         return resp, response_body
 
     def delete_server(self, instance_id, body):
-        resp, response_body = self.delete('/instance/{}'.format(instance_id), body=body)
+        resp, response_body = self.delete('instance/{}'.format(instance_id), body=body)
         return resp, response_body
 
     def get_volume_type(self, volume_type_id):
@@ -44,28 +45,28 @@ class AlmanachClient(rest_client.RestClient):
         return resp, response_body
 
     def create_volume_type(self, body):
-        resp, response_body = self.post('/volume_type', body)
+        resp, response_body = self.post('volume_type', body)
         return resp, response_body
 
     def create_volume(self, tenant_id, body):
-        url = '/project/{}/volume'.format(tenant_id)
+        url = 'project/{}/volume'.format(tenant_id)
         resp, response_body = self.post(url, body)
         return resp, response_body
 
     def delete_volume(self, volume_id, body):
-        resp, response_body = self.delete('/volume/{}'.format(volume_id), body=body)
+        resp, response_body = self.delete('volume/{}'.format(volume_id), body=body)
         return resp, response_body
 
     def resize_volume(self, volume_id, body):
-        resp, response_body = self.put('/volume/{}/resize'.format(volume_id), body)
+        resp, response_body = self.put('volume/{}/resize'.format(volume_id), body)
         return resp, response_body
 
     def attach_volume(self, volume_id, body):
-        resp, response_body = self.put('/volume/{}/attach'.format(volume_id), body)
+        resp, response_body = self.put('volume/{}/attach'.format(volume_id), body)
         return resp, response_body
 
     def detach_volume(self, volume_id, body):
-        resp, response_body = self.put('/volume/{}/detach'.format(volume_id), body)
+        resp, response_body = self.put('volume/{}/detach'.format(volume_id), body)
         return resp, response_body
 
     def get_tenant_entities(self, tenant_id):
@@ -74,16 +75,16 @@ class AlmanachClient(rest_client.RestClient):
         return resp, response_body
 
     def update_server(self, instance_id, body):
-        url = '/entity/instance/{}'.format(instance_id)
+        url = 'entity/instance/{}'.format(instance_id)
         resp, response_body = self.put(url, body)
         return resp, response_body
 
     def rebuild(self, instance_id, body):
-        update_instance_rebuild_query = "/instance/{}/rebuild".format(instance_id)
+        update_instance_rebuild_query = "instance/{}/rebuild".format(instance_id)
         resp, response_body = self.put(update_instance_rebuild_query, body)
         return resp, response_body
 
     def resize(self, instance_id, body):
-        url = "/instance/{}/resize".format(instance_id)
+        url = "instance/{}/resize".format(instance_id)
         resp, response_body = self.put(url, body)
         return resp, response_body
diff --git a/almanach/tests/unit/api/v1/test_api_authentication.py b/almanach/tests/unit/api/v1/test_api_authentication.py
index 241c2cd..f0a52fc 100644
--- a/almanach/tests/unit/api/v1/test_api_authentication.py
+++ b/almanach/tests/unit/api/v1/test_api_authentication.py
@@ -26,7 +26,7 @@ class TestApiAuthentication(base_api.BaseApi):
         self.auth_adapter.validate.side_effect = exception.AuthenticationFailureException('Unauthorized')
         query_string = {'start': '2014-01-01 00:00:00.0000', 'end': '2014-02-01 00:00:00.0000'}
 
-        code, result = self.api_get(url='/project/TENANT_ID/entities',
+        code, result = self.api_get(url='/v1/project/TENANT_ID/entities',
                                     query_string=query_string,
                                     headers={'X-Auth-Token': 'wrong token'})
 
diff --git a/almanach/tests/unit/api/v1/test_api_entity.py b/almanach/tests/unit/api/v1/test_api_entity.py
index c7af136..e5188b3 100644
--- a/almanach/tests/unit/api/v1/test_api_entity.py
+++ b/almanach/tests/unit/api/v1/test_api_entity.py
@@ -35,7 +35,7 @@ class TestApiEntity(base_api.BaseApi):
         end = '2016-03-03 00:00:00.000000'
 
         code, result = self.api_put(
-            '/entity/instance/INSTANCE_ID',
+            '/v1/entity/instance/INSTANCE_ID',
             headers={'X-Auth-Token': 'some token value'},
             query_string={
                 'start': start,
@@ -63,7 +63,7 @@ class TestApiEntity(base_api.BaseApi):
         self.entity_ctl.update_active_instance_entity.return_value = an_instance
 
         code, result = self.api_put(
-            '/entity/instance/INSTANCE_ID',
+            '/v1/entity/instance/INSTANCE_ID',
             headers={'X-Auth-Token': 'some token value'},
             data=data,
         )
@@ -87,11 +87,9 @@ class TestApiEntity(base_api.BaseApi):
             'flavor': 'A_FLAVOR',
         }
 
-        code, result = self.api_put(
-                '/entity/instance/INSTANCE_ID',
-                data=data,
-                headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_put('/v1/entity/instance/INSTANCE_ID',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.entity_ctl.update_active_instance_entity.assert_called_once_with(instance_id=instance_id, **data)
         self.assertIn("error", result)
@@ -115,11 +113,9 @@ class TestApiEntity(base_api.BaseApi):
             'flavor': 'A_FLAVOR',
         }
 
-        code, result = self.api_put(
-            '/entity/instance/INSTANCE_ID',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_put('/v1/entity/instance/INSTANCE_ID',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.entity_ctl.update_active_instance_entity.assert_called_once_with(instance_id=instance_id, **data)
         self.assertIn("error", result)
@@ -130,7 +126,8 @@ class TestApiEntity(base_api.BaseApi):
         self.entity_ctl.entity_exists.return_value = True
         entity_id = "entity_id"
 
-        code, result = self.api_head('/entity/{id}'.format(id=entity_id), headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_head('/v1/entity/{id}'.format(id=entity_id),
+                                     headers={'X-Auth-Token': 'some token value'})
 
         self.entity_ctl.entity_exists.assert_called_once_with(entity_id=entity_id)
         self.assertEqual(code, 200)
@@ -139,7 +136,8 @@ class TestApiEntity(base_api.BaseApi):
         self.entity_ctl.entity_exists.return_value = False
         entity_id = "entity_id"
 
-        code, result = self.api_head('/entity/{id}'.format(id=entity_id), headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_head('/v1/entity/{id}'.format(id=entity_id),
+                                     headers={'X-Auth-Token': 'some token value'})
 
         self.entity_ctl.entity_exists.assert_called_once_with(entity_id=entity_id)
         self.assertEqual(code, 404)
diff --git a/almanach/tests/unit/api/v1/test_api_info.py b/almanach/tests/unit/api/v1/test_api_info.py
index fd16a04..8ce92f5 100644
--- a/almanach/tests/unit/api/v1/test_api_info.py
+++ b/almanach/tests/unit/api/v1/test_api_info.py
@@ -18,10 +18,16 @@ from almanach.tests.unit.api.v1 import base_api
 class TestApiInfo(base_api.BaseApi):
 
     def test_info(self):
+        self.assert_info_call('/v1/info')
+
+    def test_info_with_legacy_url(self):
+        self.assert_info_call('/info')
+
+    def assert_info_call(self, url):
         info = {'info': {'version': '1.0'}, 'database': {'all_entities': 10, 'active_entities': 2}}
         self.app_ctl.get_application_info.return_value = info
 
-        code, result = self.api_get('/info')
+        code, result = self.api_get(url)
 
         self.app_ctl.get_application_info.assert_called_once()
         self.assertEqual(code, 200)
diff --git a/almanach/tests/unit/api/v1/test_api_instance.py b/almanach/tests/unit/api/v1/test_api_instance.py
index 66508a2..4c24026 100644
--- a/almanach/tests/unit/api/v1/test_api_instance.py
+++ b/almanach/tests/unit/api/v1/test_api_instance.py
@@ -22,7 +22,7 @@ class TestApiInstance(base_api.BaseApi):
 
     def test_get_instances(self):
         self.instance_ctl.list_instances.return_value = [a(instance().with_id('123'))]
-        code, result = self.api_get('/project/TENANT_ID/instances',
+        code, result = self.api_get('/v1/project/TENANT_ID/instances',
                                     query_string={
                                         'start': '2014-01-01 00:00:00.0000',
                                         'end': '2014-02-01 00:00:00.0000'
@@ -47,11 +47,9 @@ class TestApiInstance(base_api.BaseApi):
                     os_distro="A_DISTRIBUTION",
                     os_version="AN_OS_VERSION")
 
-        code, result = self.api_post(
-            '/project/PROJECT_ID/instance',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_post('/v1/project/PROJECT_ID/instance',
+                                     data=data,
+                                     headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.create_instance.assert_called_once_with(
             tenant_id="PROJECT_ID",
@@ -73,11 +71,9 @@ class TestApiInstance(base_api.BaseApi):
                     os_type="AN_OS_TYPE",
                     os_version="AN_OS_VERSION")
 
-        code, result = self.api_post(
-            '/project/PROJECT_ID/instance',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_post('/v1/project/PROJECT_ID/instance',
+                                     data=data,
+                                     headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.create_instance.assert_not_called()
         self.assertEqual(result["error"], "The 'os_distro' param is mandatory for the request you have made.")
@@ -93,11 +89,9 @@ class TestApiInstance(base_api.BaseApi):
                     os_distro="A_DISTRIBUTION",
                     os_version="AN_OS_VERSION")
 
-        code, result = self.api_post(
-            '/project/PROJECT_ID/instance',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_post('/v1/project/PROJECT_ID/instance',
+                                     data=data,
+                                     headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.create_instance.assert_called_once_with(
             tenant_id="PROJECT_ID",
@@ -122,7 +116,7 @@ class TestApiInstance(base_api.BaseApi):
                     flavor="A_FLAVOR")
 
         code, result = self.api_put(
-            '/instance/INSTANCE_ID/resize',
+            '/v1/instance/INSTANCE_ID/resize',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -137,7 +131,9 @@ class TestApiInstance(base_api.BaseApi):
     def test_successfull_instance_delete(self):
         data = dict(date="DELETE_DATE")
 
-        code, result = self.api_delete('/instance/INSTANCE_ID', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/instance/INSTANCE_ID',
+                                       data=data,
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.delete_instance.assert_called_once_with(
             instance_id="INSTANCE_ID",
@@ -147,7 +143,7 @@ class TestApiInstance(base_api.BaseApi):
 
     def test_instance_delete_missing_a_param_returns_bad_request_code(self):
         code, result = self.api_delete(
-            '/instance/INSTANCE_ID',
+            '/v1/instance/INSTANCE_ID',
             data=dict(),
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -160,7 +156,7 @@ class TestApiInstance(base_api.BaseApi):
         self.assertEqual(code, 400)
 
     def test_instance_delete_no_data_bad_request_code(self):
-        code, result = self.api_delete('/instance/INSTANCE_ID', headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/instance/INSTANCE_ID', headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.delete_instance.assert_not_called()
         self.assertIn(
@@ -173,7 +169,9 @@ class TestApiInstance(base_api.BaseApi):
         data = dict(date="A_BAD_DATE")
         self.instance_ctl.delete_instance.side_effect = exception.DateFormatException
 
-        code, result = self.api_delete('/instance/INSTANCE_ID', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/instance/INSTANCE_ID',
+                                       data=data,
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.delete_instance.assert_called_once_with(
             instance_id="INSTANCE_ID",
@@ -189,7 +187,7 @@ class TestApiInstance(base_api.BaseApi):
     def test_instance_resize_missing_a_param_returns_bad_request_code(self):
         data = dict(date="UPDATED_AT")
         code, result = self.api_put(
-            '/instance/INSTANCE_ID/resize',
+            '/v1/instance/INSTANCE_ID/resize',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -206,7 +204,7 @@ class TestApiInstance(base_api.BaseApi):
         data = dict(date="A_BAD_DATE",
                     flavor="A_FLAVOR")
         code, result = self.api_put(
-            '/instance/INSTANCE_ID/resize',
+            '/v1/instance/INSTANCE_ID/resize',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -231,11 +229,9 @@ class TestApiInstance(base_api.BaseApi):
             'os_type': 'AN_OS_TYPE',
             'rebuild_date': 'UPDATE_DATE',
         }
-        code, result = self.api_put(
-            '/instance/INSTANCE_ID/rebuild',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_put('/v1/instance/INSTANCE_ID/rebuild',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.rebuild_instance.assert_called_once_with(
             instance_id=instance_id,
@@ -251,11 +247,9 @@ class TestApiInstance(base_api.BaseApi):
             'distro': 'A_DISTRIBUTION',
             'rebuild_date': 'UPDATE_DATE',
         }
-        code, result = self.api_put(
-            '/instance/INSTANCE_ID/rebuild',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_put('/v1/instance/INSTANCE_ID/rebuild',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.rebuild_instance.assert_not_called()
         self.assertIn(
@@ -273,11 +267,9 @@ class TestApiInstance(base_api.BaseApi):
             'os_type': 'AN_OS_TYPE',
             'rebuild_date': 'A_BAD_UPDATE_DATE',
         }
-        code, result = self.api_put(
-            '/instance/INSTANCE_ID/rebuild',
-            data=data,
-            headers={'X-Auth-Token': 'some token value'}
-        )
+        code, result = self.api_put('/v1/instance/INSTANCE_ID/rebuild',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.instance_ctl.rebuild_instance.assert_called_once_with(
             instance_id=instance_id,
diff --git a/almanach/tests/unit/api/v1/test_api_volume.py b/almanach/tests/unit/api/v1/test_api_volume.py
index 5cbb8ca..7ccc84f 100644
--- a/almanach/tests/unit/api/v1/test_api_volume.py
+++ b/almanach/tests/unit/api/v1/test_api_volume.py
@@ -29,7 +29,7 @@ class TestApiVolume(base_api.BaseApi):
                     attached_to=["INSTANCE_ID"])
 
         code, result = self.api_post(
-            '/project/PROJECT_ID/volume',
+            '/v1/project/PROJECT_ID/volume',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -48,7 +48,7 @@ class TestApiVolume(base_api.BaseApi):
                     attached_to=[])
 
         code, result = self.api_post(
-            '/project/PROJECT_ID/volume',
+            '/v1/project/PROJECT_ID/volume',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -70,7 +70,7 @@ class TestApiVolume(base_api.BaseApi):
                     attached_to=["INSTANCE_ID"])
 
         code, result = self.api_post(
-            '/project/PROJECT_ID/volume',
+            '/v1/project/PROJECT_ID/volume',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -89,7 +89,9 @@ class TestApiVolume(base_api.BaseApi):
     def test_successful_volume_delete(self):
         data = dict(date="DELETE_DATE")
 
-        code, result = self.api_delete('/volume/VOLUME_ID', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume/VOLUME_ID',
+                                       data=data,
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.volume_ctl.delete_volume.assert_called_once_with(
             volume_id="VOLUME_ID",
@@ -98,7 +100,9 @@ class TestApiVolume(base_api.BaseApi):
         self.assertEqual(code, 202)
 
     def test_volume_delete_missing_a_param_returns_bad_request_code(self):
-        code, result = self.api_delete('/volume/VOLUME_ID', data=dict(), headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume/VOLUME_ID',
+                                       data=dict(),
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The 'date' param is mandatory for the request you have made.",
@@ -108,7 +112,8 @@ class TestApiVolume(base_api.BaseApi):
         self.assertEqual(code, 400)
 
     def test_volume_delete_no_data_bad_request_code(self):
-        code, result = self.api_delete('/volume/VOLUME_ID', headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume/VOLUME_ID',
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "Invalid parameter or payload",
@@ -121,7 +126,9 @@ class TestApiVolume(base_api.BaseApi):
         self.volume_ctl.delete_volume.side_effect = exception.DateFormatException
         data = dict(date="A_BAD_DATE")
 
-        code, result = self.api_delete('/volume/VOLUME_ID', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume/VOLUME_ID',
+                                       data=data,
+                                       headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The provided date has an invalid format. "
@@ -138,7 +145,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="UPDATED_AT",
                     size="NEW_SIZE")
 
-        code, result = self.api_put('/volume/VOLUME_ID/resize', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/resize',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.volume_ctl.resize_volume.assert_called_once_with(
             volume_id="VOLUME_ID",
@@ -150,7 +159,9 @@ class TestApiVolume(base_api.BaseApi):
     def test_volume_resize_missing_a_param_returns_bad_request_code(self):
         data = dict(date="A_DATE")
 
-        code, result = self.api_put('/volume/VOLUME_ID/resize', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/resize',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The 'size' param is mandatory for the request you have made.",
@@ -164,7 +175,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="BAD_DATE",
                     size="NEW_SIZE")
 
-        code, result = self.api_put('/volume/VOLUME_ID/resize', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/resize',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The provided date has an invalid format. "
@@ -182,7 +195,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="UPDATED_AT",
                     attachments=[uuidutils.generate_uuid()])
 
-        code, result = self.api_put('/volume/VOLUME_ID/attach', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/attach',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.volume_ctl.attach_volume.assert_called_once_with(
             volume_id="VOLUME_ID",
@@ -195,7 +210,7 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="A_DATE")
 
         code, result = self.api_put(
-            '/volume/VOLUME_ID/attach',
+            '/v1/volume/VOLUME_ID/attach',
             data=data,
             headers={'X-Auth-Token': 'some token value'}
         )
@@ -212,7 +227,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="A_BAD_DATE",
                     attachments=[uuidutils.generate_uuid()])
 
-        code, result = self.api_put('/volume/VOLUME_ID/attach', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/attach',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The provided date has an invalid format. "
@@ -230,7 +247,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="UPDATED_AT",
                     attachments=[uuidutils.generate_uuid()])
 
-        code, result = self.api_put('/volume/VOLUME_ID/detach', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/detach',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.volume_ctl.detach_volume.assert_called_once_with(
             volume_id="VOLUME_ID",
@@ -242,7 +261,9 @@ class TestApiVolume(base_api.BaseApi):
     def test_volume_detach_missing_a_param_returns_bad_request_code(self):
         data = dict(date="A_DATE")
 
-        code, result = self.api_put('/volume/VOLUME_ID/detach', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/detach',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The 'attachments' param is mandatory for the request you have made.",
@@ -256,7 +277,9 @@ class TestApiVolume(base_api.BaseApi):
         data = dict(date="A_BAD_DATE",
                     attachments=[uuidutils.generate_uuid()])
 
-        code, result = self.api_put('/volume/VOLUME_ID/detach', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_put('/v1/volume/VOLUME_ID/detach',
+                                    data=data,
+                                    headers={'X-Auth-Token': 'some token value'})
 
         self.assertIn(
             "The provided date has an invalid format. "
diff --git a/almanach/tests/unit/api/v1/test_api_volume_type.py b/almanach/tests/unit/api/v1/test_api_volume_type.py
index c98605c..a5da9fa 100644
--- a/almanach/tests/unit/api/v1/test_api_volume_type.py
+++ b/almanach/tests/unit/api/v1/test_api_volume_type.py
@@ -25,7 +25,7 @@ class TestApiVolumeType(base_api.BaseApi):
             a(volume_type().with_volume_type_name('some_volume_type_name'))
         ]
 
-        code, result = self.api_get('/volume_types', headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_get('/v1/volume_types', headers={'X-Auth-Token': 'some token value'})
 
         self.volume_type_ctl.list_volume_types.assert_called_once()
         self.assertEqual(code, 200)
@@ -39,7 +39,7 @@ class TestApiVolumeType(base_api.BaseApi):
             type_name="A_VOLUME_TYPE_NAME"
         )
 
-        code, result = self.api_post('/volume_type', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_post('/v1/volume_type', data=data, headers={'X-Auth-Token': 'some token value'})
 
         self.volume_type_ctl.create_volume_type.assert_called_once_with(
             volume_type_id=data['type_id'],
@@ -50,14 +50,14 @@ class TestApiVolumeType(base_api.BaseApi):
     def test_volume_type_create_missing_a_param_returns_bad_request_code(self):
         data = dict(type_name="A_VOLUME_TYPE_NAME")
 
-        code, result = self.api_post('/volume_type', data=data, headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_post('/v1/volume_type', data=data, headers={'X-Auth-Token': 'some token value'})
 
         self.volume_type_ctl.create_volume_type.assert_not_called()
         self.assertEqual(result["error"], "The 'type_id' param is mandatory for the request you have made.")
         self.assertEqual(code, 400)
 
     def test_volume_type_delete_with_authentication(self):
-        code, result = self.api_delete('/volume_type/A_VOLUME_TYPE_ID', headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume_type/A_VOLUME_TYPE_ID', headers={'X-Auth-Token': 'some token value'})
 
         self.volume_type_ctl.delete_volume_type.assert_called_once_with('A_VOLUME_TYPE_ID')
         self.assertEqual(code, 202)
@@ -65,7 +65,7 @@ class TestApiVolumeType(base_api.BaseApi):
     def test_volume_type_delete_not_in_database(self):
         self.volume_type_ctl.delete_volume_type.side_effect = exception.AlmanachException("An exception occurred")
 
-        code, result = self.api_delete('/volume_type/A_VOLUME_TYPE_ID', headers={'X-Auth-Token': 'some token value'})
+        code, result = self.api_delete('/v1/volume_type/A_VOLUME_TYPE_ID', headers={'X-Auth-Token': 'some token value'})
 
         self.volume_type_ctl.delete_volume_type.assert_called_once_with('A_VOLUME_TYPE_ID')
         self.assertIn("An exception occurred", result["error"])
diff --git a/doc/source/index.rst b/doc/source/index.rst
index aa5561a..06f1db0 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -244,10 +244,10 @@ Almanach will process those events:
 - :code:`volume.exists`
 - :code:`volume_type.create`
 
-API documentation
------------------
+API v1 Documentation
+--------------------
 
-:code:`GET /volume_types`
+:code:`GET /v1/volume_types`
 
     List volume types.
 
@@ -260,7 +260,7 @@ API documentation
         .. literalinclude:: api_examples/output/volume_types.json
             :language: json
 
-:code:`GET /volume_type/<volume_type_id>`
+:code:`GET /v1/volume_type/<volume_type_id>`
 
     Get a volume type.
 
@@ -291,7 +291,7 @@ API documentation
         .. literalinclude:: api_examples/output/volume_type.json
             :language: json
 
-:code:`POST /volume_type`
+:code:`POST /v1/volume_type`
 
     Create a volume type.
 
@@ -324,7 +324,7 @@ API documentation
         .. literalinclude:: api_examples/input/create_volume_type-body.json
             :language: json
 
-:code:`DELETE /volume_type/<volume_type_id>`
+:code:`DELETE /v1/volume_type/<volume_type_id>`
 
     Delete a volume type.
 
@@ -348,7 +348,7 @@ API documentation
               - uuid
               - The Volume Type Uuid
 
-:code:`GET /info`
+:code:`GET /v1/info`
 
     Display information about the current version and entity counts.
 
@@ -361,7 +361,7 @@ API documentation
         .. literalinclude:: api_examples/output/info.json
             :language: json
 
-:code:`POST /project/<project_id>/instance`
+:code:`POST /v1/project/<project_id>/instance`
 
     Create an instance.
 
@@ -419,7 +419,7 @@ API documentation
         .. literalinclude:: api_examples/input/create_instance-body.json
             :language: json
 
-:code:`DELETE /instance/<instance_id>`
+:code:`DELETE /v1/instance/<instance_id>`
 
     Delete an instance.
 
@@ -453,7 +453,7 @@ API documentation
         .. literalinclude:: api_examples/input/delete_instance-body.json
             :language: json
 
-:code:`PUT /instance/<instance_id>/resize`
+:code:`PUT /v1/instance/<instance_id>/resize`
 
     Re-size an instance.
 
@@ -491,7 +491,7 @@ API documentation
         .. literalinclude:: api_examples/input/resize_instance-body.json
             :language: json
 
-:code:`PUT /instance/<instance_id>/rebuild`
+:code:`PUT /v1/instance/<instance_id>/rebuild`
 
     Rebuild an instance.
 
@@ -537,7 +537,7 @@ API documentation
         .. literalinclude:: api_examples/input/rebuild_instance-body.json
             :language: json
 
-:code:`GET /project/<project_id>/instances`
+:code:`GET /v1/project/<project_id>/instances`
 
     List instances for a tenant.
 
@@ -575,7 +575,7 @@ API documentation
         .. literalinclude:: api_examples/output/instances.json
             :language: json
 
-:code:`POST /project/<project_id>/volume`
+:code:`POST /v1/project/<project_id>/volume`
 
     Create a volume.
 
@@ -629,7 +629,7 @@ API documentation
         .. literalinclude:: api_examples/input/create_volume-body.json
             :language: json
 
-:code:`DELETE /volume/<volume_id>`
+:code:`DELETE /v1/volume/<volume_id>`
 
     Delete a volume.
 
@@ -663,7 +663,7 @@ API documentation
         .. literalinclude:: api_examples/input/delete_volume-body.json
             :language: json
 
-:code:`PUT /volume/<volume_id>/resize`
+:code:`PUT /v1/volume/<volume_id>/resize`
 
     Re-size a volume.
 
@@ -701,7 +701,7 @@ API documentation
         .. literalinclude:: api_examples/input/resize_volume-body.json
             :language: json
 
-:code:`PUT /volume/<volume_id>/attach`
+:code:`PUT /v1/volume/<volume_id>/attach`
 
     Update the attachments for a volume.
 
@@ -739,7 +739,7 @@ API documentation
         .. literalinclude:: api_examples/input/attach_volume-body.json
             :language: json
 
-:code:`PUT /volume/<volume_id>/detach`
+:code:`PUT /v1/volume/<volume_id>/detach`
 
     Detach a volume.
 
@@ -777,7 +777,7 @@ API documentation
         .. literalinclude:: api_examples/input/detach_volume-body.json
             :language: json
 
-:code:`GET /project/<project_id>/volumes`
+:code:`GET /v1/project/<project_id>/volumes`
 
     List volumes for a tenant.
 
@@ -815,7 +815,7 @@ API documentation
         .. literalinclude:: api_examples/output/volumes.json
             :language: json
 
-:code:`GET /project/<project_id>/entities`
+:code:`GET /v1/project/<project_id>/entities`
 
     List entities for a tenant.
 
@@ -853,7 +853,7 @@ API documentation
         .. literalinclude:: api_examples/output/entities.json
             :language: json
 
-:code:`PUT /entity/instance/<instance_id>`
+:code:`PUT /v1/entity/instance/<instance_id>`
 
     Update an instance.
 
@@ -896,7 +896,7 @@ API documentation
         .. literalinclude:: api_examples/output/update_instance_entity.json
             :language: json
 
-:code:`HEAD /entity/<entity_id>`
+:code:`HEAD /v1/entity/<entity_id>`
 
     Verify that an entity exists.
 
@@ -925,7 +925,7 @@ API documentation
         .. literalinclude:: api_examples/output/entity.json
             :language: json
 
-:code:`GET /entity/<entity_id>`
+:code:`GET /v1/entity/<entity_id>`
 
     Get an entity.