diff --git a/ceagle/api/client.py b/ceagle/api/client.py index d26abb6..eafc29d 100644 --- a/ceagle/api/client.py +++ b/ceagle/api/client.py @@ -32,27 +32,70 @@ class UnknownService(Exception): class Client(base.Client): - def get(self, uri="/", **kwargs): - """Make GET request and decode JSON data. + def request(self, method="GET", uri="/", **kwargs): + """Make `method` request and decode JSON data. :param uri: resource URI :param kwargs: query parameters - :returns: dict response data + :returns: tuple: response data-dict and response code """ url = "%s%s" % (self.endpoint, uri) try: - response = requests.get(url, **kwargs) + response = requests.request(method, url, **kwargs) except requests.exceptions.ConnectionError: mesg = "Service '%(name)s' is not available at '%(endpoint)s'" % ( {"name": self.name, "endpoint": self.endpoint}) return {"error": {"message": mesg}}, 502 + + # NO_CONTENT means we have nothing to decode + # expected for DELETE methods for example + if response.status_code == 204: + return "", response.status_code + try: result = response.json() except ValueError: - return {"error": {"message": "Response can not be decoded"}}, 500 + return {"error": { + "message": "Response can not be decoded"}}, 500 return result, response.status_code + def get(self, uri="/", **kwargs): + """Make GET request and decode JSON data. + + :param uri: resource URI + :param kwargs: query parameters + :returns: tuple: response data-dict and response code + """ + return self.request("GET", uri, **kwargs) + + def post(self, uri="/", **kwargs): + """Make POST request and decode JSON data. + + :param uri: resource URI + :param kwargs: query parameters + :returns: tuple: response data-dict and response code + """ + return self.request("POST", uri, **kwargs) + + def put(self, uri="/", **kwargs): + """Make PUT request and decode JSON data. + + :param uri: resource URI + :param kwargs: query parameters + :returns: tuple: response data-dict and response code + """ + return self.request("PUT", uri, **kwargs) + + def delete(self, uri="/", **kwargs): + """Make DELETE request and decode JSON data. + + :param uri: resource URI + :param kwargs: query parameters + :returns: tuple: response data-dict and response code + """ + return self.request("DELETE", uri, **kwargs) + def get_client(service_name): """Return client for given service name, if possible. diff --git a/ceagle/api/v1/runbooks.py b/ceagle/api/v1/runbooks.py index a535be7..cdfcf17 100644 --- a/ceagle/api/v1/runbooks.py +++ b/ceagle/api/v1/runbooks.py @@ -15,6 +15,7 @@ import flask +from ceagle.api import client from ceagle.api_fake_data import fake_runbooks @@ -25,35 +26,75 @@ @bp.route("/region//runbooks", methods=["GET", "POST"]) @fake_runbooks.handle_runbooks -def handle_runbooks(region=None): - return flask.jsonify("fixme!") +def handle_runbooks(region=''): + api_endpoint = "/api/v1/region/{}/runbooks".format(region) + + if flask.request.method == "GET": + read_client = client.get_client("runbook-read") + if not region: + api_endpoint = "/api/v1/runbooks" + params = flask.request.args + result, code = read_client.get(api_endpoint, params=params) + else: # POST + write_client = client.get_client("runbook-write") + new_runbook = flask.request.get_json(silent=True) or {} + result, code = write_client.post(api_endpoint, json=new_runbook) + return flask.jsonify(result), code @bp.route("/region//runbooks/", methods=["GET", "PUT", "DELETE"]) @fake_runbooks.handle_single_runbook def handle_single_runbook(region, book_id): - return flask.jsonify("fixme!") + api_endpoint = "/api/v1/region/{}/runbooks/{}".format( + region, book_id) + + if flask.request.method == "GET": + read_client = client.get_client("runbook-read") + result, code = read_client.get(api_endpoint) + elif flask.request.method == "PUT": + write_client = client.get_client("runbook-write") + new_runbook = flask.request.get_json(silent=True) or {} + result, code = write_client.put(api_endpoint, json=new_runbook) + elif flask.request.method == "DELETE": + write_client = client.get_client("runbook-write") + result, code = write_client.delete(api_endpoint) + return flask.jsonify(result), code @bp.route("/region//runbooks//run", methods=["POST"]) @fake_runbooks.run_runbook def run_runbook(region, book_id): - return flask.jsonify("fixme!") + run_client = client.get_client("runbook-run") + run_settings = flask.request.get_json(silent=True) or {} + api_endpoint = "/api/v1/region/{}/runbooks/{}/run".format(region, book_id) + result, code = run_client.post(api_endpoint, json=run_settings) + return flask.jsonify(result), code @bp.route("/runbook_runs") @bp.route("/region//runbook_runs") @fake_runbooks.runbook_runs def runbook_runs(region=None): - return flask.jsonify("fixme!") + read_client = client.get_client("runbook-read") + if region: + api_endpoint = "/api/v1/region/{}/runbook_runs".format(region) + else: + api_endpoint = "/api/v1/runbook_runs" + params = flask.request.args + result, code = read_client.get(api_endpoint, params=params) + return flask.jsonify(result), code @bp.route("/region//runbook_runs/") @fake_runbooks.single_runbook_run def single_runbook_run(region, run_id): - return flask.jsonify("fixme!") + read_client = client.get_client("runbook-read") + api_endpoint = "/api/v1/region/{}/runbook_runs/{}".format(region, run_id) + params = flask.request.args + result, code = read_client.get(api_endpoint, params=params) + return flask.jsonify(result), code def get_blueprints(): diff --git a/ceagle/config.py b/ceagle/config.py index 50e3a17..56a6fc7 100644 --- a/ceagle/config.py +++ b/ceagle/config.py @@ -31,6 +31,9 @@ "health": {"type": "string"}, "optimization": {"type": "string"}, "performance": {"type": "string"}, + "runbook-read": {"type": "string"}, + "runbook-write": {"type": "string"}, + "runbook-run": {"type": "string"}, "security": {"type": "string"}, "infra": { "type": "object", diff --git a/tests/unit/api/test_client.py b/tests/unit/api/test_client.py index 9a11f6b..e83a38f 100644 --- a/tests/unit/api/test_client.py +++ b/tests/unit/api/test_client.py @@ -30,41 +30,74 @@ def test___init__(self): self.assertEqual("foo_ep", ct.endpoint) self.assertEqual("", repr(ct)) - @mock.patch("ceagle.api.client.requests.get") - def test_get(self, mock_requests_get): - mock_requests_get.return_value.status_code = "foo_status" - mock_requests_get.return_value.json.return_value = {"foo": 42} + @mock.patch("ceagle.api.client.requests.request") + def test_get(self, mock_requests): + mock_requests.return_value.status_code = "foo_status" + mock_requests.return_value.json.return_value = {"foo": 42} ct = client.Client("foo", "http://foo_ep") result = ct.get() - mock_requests_get.assert_called_once_with("http://foo_ep/") + mock_requests.assert_called_once_with("GET", "http://foo_ep/") self.assertEqual(({"foo": 42}, "foo_status"), result) - mock_requests_get.reset_mock() - - @mock.patch("ceagle.api.client.requests.get") - def test_get_with_path(self, mock_requests_get): - mock_requests_get.return_value.json.return_value = {"foo": 42} - mock_requests_get.return_value.status_code = 200 + @mock.patch("ceagle.api.client.requests.request") + def test_get_with_path(self, mock_requests): + mock_requests.return_value.json.return_value = {"foo": 42} + mock_requests.return_value.status_code = 200 ct = client.Client("foo", "http://foo_ep") result = ct.get("/bar") - mock_requests_get.assert_called_once_with("http://foo_ep/bar") + mock_requests.assert_called_once_with("GET", "http://foo_ep/bar") self.assertEqual(({"foo": 42}, 200), result) - @mock.patch("ceagle.api.client.requests.get") - def test_get_wrong_response_fmt(self, mock_requests_get): - mock_requests_get.return_value.json.side_effect = ValueError + @mock.patch("ceagle.api.client.requests.request") + def test_get_no_content(self, mock_requests): + mock_requests.return_value.json.return_value = {"foo": 42} + mock_requests.return_value.status_code = 204 + ct = client.Client("foo", "http://foo_ep") + result = ct.get("/bar") + mock_requests.assert_called_once_with("GET", "http://foo_ep/bar") + self.assertEqual(("", 204), result) + + @mock.patch("ceagle.api.client.requests.request") + def test_get_wrong_response_fmt(self, mock_requests): + mock_requests.return_value.json.side_effect = ValueError ct = client.Client("foo", "http://foo_ep") result = ct.get("/bar") self.assertEqual( ({"error": {"message": "Response can not be decoded"}}, 500), result) - @mock.patch("ceagle.api.client.requests.get") - def test_get_not_available(self, mock_requests_get): - mock_requests_get.side_effect = ( + @mock.patch("ceagle.api.client.requests.request") + def test_get_not_available(self, mock_requests): + mock_requests.side_effect = ( client.requests.exceptions.ConnectionError) ct = client.Client("foo", "http://foo_ep") result = ct.get("/bar") mesg = "Service 'foo' is not available at 'http://foo_ep'" self.assertEqual(({"error": {"message": mesg}}, 502), result) + + @mock.patch("ceagle.api.client.requests.request") + def test_methods(self, mock_requests): + mock_requests.return_value.json.return_value = {"foo": 42} + mock_requests.return_value.status_code = 200 + ct = client.Client("foo", "http://foo_ep") + + result = ct.get("/bar", body={}, data={}) + mock_requests.assert_called_with("GET", "http://foo_ep/bar", + body={}, data={}) + self.assertEqual(({"foo": 42}, 200), result) + + result = ct.put("/bar", body={}, data={}) + mock_requests.assert_called_with("PUT", "http://foo_ep/bar", + body={}, data={}) + self.assertEqual(({"foo": 42}, 200), result) + + result = ct.post("/bar", body={}, data={}) + mock_requests.assert_called_with("POST", "http://foo_ep/bar", + body={}, data={}) + self.assertEqual(({"foo": 42}, 200), result) + + result = ct.delete("/bar", body={}, data={}) + mock_requests.assert_called_with("DELETE", "http://foo_ep/bar", + body={}, data={}) + self.assertEqual(({"foo": 42}, 200), result)