Skip to content
This repository has been archived by the owner on May 24, 2023. It is now read-only.

Commit

Permalink
Add health check endpoint
Browse files Browse the repository at this point in the history
Endpoint '/v1/health/ping' provides info about service status

* OSBS-6692

Signed-off-by: Martin Bašti <mbasti@redhat.com>
  • Loading branch information
MartinBasti committed Mar 15, 2019
1 parent 231c06f commit b98c34b
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 2 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,71 @@ curl \
-X DELETE https://example.com/v1/myorg/myrepo/1.1.5
```

### Health status

Check status of OMPS service and accessibility of:
* quay service
* koji service

If HTTP status is not 200, consider service as not working.

#### Endpoints

* [GET] `/v1/health/ping`


#### Replies

**OK**

HTTP code: 200

```json
{
"ok": true,
"services": {
"koji": {
"details": "It works!",
"ok": true
},
"quay": {
"details": "It works!",
"ok": true
}
},
"status": 200
}
```

**Failure**

HTTP code: other than 200

Format of message cannot be guaranteed. However service will try to answer with
the same format as is listed above, for example:

```json
{
"ok": false,
"services": {
"koji": {
"details": "Cannot connect!",
"ok": false
},
"quay": {
"details": "It works!",
"ok": true
}
},
"status": 503
}
```


#### Examples
```bash
curl https://example.com/v1/health/ping
```

## Development

Expand Down
2 changes: 1 addition & 1 deletion omps/api/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

API = Blueprint('v1', __name__)

from . import packages, push # noqa, register routes
from . import packages, push, health # noqa, register routes
79 changes: 79 additions & 0 deletions omps/api/v1/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

import logging

from flask import jsonify
import requests
from requests.exceptions import RequestException

from . import API
from omps.errors import KojiError
from omps.quay import get_cnr_api_version
from omps.koji_util import KOJI

logger = logging.getLogger(__name__)


@API.route("/health/ping", methods=('GET', ))
def ping():
"""Provides status report
* 200 if everything is ok
* 503 if service is not working as expected
:return: HTTP Response
"""
def _ok():
return {
"ok": True,
"details": "It works!",
}

def _err(exc):
return {
"ok": False,
"details": str(exc)
}

everything_ok = True

# quay.io status
try:
# try to retrieve API version to check if quay.io is alive
get_cnr_api_version()
except RequestException as e:
logger.error('Koji version check: %s', e)
quay_result = _err(e)
everything_ok = False
else:
quay_result = _ok()

# koji status
try:
# try to retrieve API version to check if koji is alive
KOJI.get_api_version()
except KojiError as e:
logger.error('Quay version check: %s', e)
koji_result = _err(e)
everything_ok = False
else:
koji_result = _ok()

status_code = (
requests.codes.ok if everything_ok
else requests.codes.unavailable
)

data = {
"ok": everything_ok,
"status": status_code,
"services": {
"koji": koji_result,
"quay": quay_result
}
}

resp = jsonify(data)
resp.status_code = status_code
return resp
13 changes: 13 additions & 0 deletions omps/koji_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,18 @@ def download_manifest_archive(self, nvr, target_fd):
url = self._kojiroot_url + path
self._file_download(url, target_fd)

def get_api_version(self):
"""Returns API version of koji
:rtype: int
:return: Koji API version
"""
try:
ver = self.session.getAPIVersion()
except Exception as e:
raise KojiError(str(e))

return ver


KOJI = KojiUtil()
13 changes: 13 additions & 0 deletions omps/quay.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,17 @@ def publish_repo(self, repo):
raise QuayPackageError(msg)


def get_cnr_api_version():
"""Returns quay's CNR api version
:raises: HTTPError if version cannot be fetched
:rtype: str
:returns: API version
"""
url = "https://quay.io/cnr/version"
r = requests.get(url)
r.raise_for_status()
return r.json()['cnr-api']


ORG_MANAGER = OrgManager()
92 changes: 92 additions & 0 deletions tests/api/v1/test_api_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#
# Copyright (C) 2019 Red Hat, Inc
# see the LICENSE file for license
#

import requests
import requests_mock

from omps.errors import KojiError


PING_ENDPOINT = '/v1/health/ping'


def test_health_ping(client, mocked_koji_get_api_version, mocked_quay_version):
"""Test reporting of ok status"""
rv = client.get(PING_ENDPOINT)

expected = {
"ok": True,
"status": requests.codes.ok,
"services": {
"koji": {
"ok": True,
"details": "It works!"
},
"quay": {
"ok": True,
"details": "It works!"
},
}
}

assert rv.status_code == requests.codes.ok
assert rv.get_json() == expected


def test_health_ping_broken_quay(client, mocked_koji_get_api_version):
"""Test if broken quay is reported properly"""
with requests_mock.Mocker() as m:
m.get(
'https://quay.io/cnr/version',
status_code=requests.codes.server_error
)
rv = client.get(PING_ENDPOINT)

expected = {
"ok": False,
"status": requests.codes.unavailable,
"services": {
"koji": {
"ok": True,
"details": "It works!"
},
"quay": {
"ok": False,
"details": (
"500 Server Error: None for url: "
"https://quay.io/cnr/version"
)
},
}
}
assert rv.status_code == requests.codes.unavailable
assert rv.get_json() == expected


def test_health_ping_broken_koji(
client, mocked_quay_version, mocked_koji_get_api_version
):
"""Test if broken koji is reported properly"""
e_msg = "something happened"
mocked_koji_get_api_version.side_effect = KojiError(e_msg)

rv = client.get(PING_ENDPOINT)

expected = {
"ok": False,
"status": requests.codes.unavailable,
"services": {
"koji": {
"ok": False,
"details": e_msg
},
"quay": {
"ok": True,
"details": "It works!"
},
}
}
assert rv.status_code == requests.codes.unavailable
assert rv.get_json() == expected
24 changes: 24 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import re
import os
import shutil
from unittest.mock import Mock
import zipfile

from flexmock import flexmock
Expand Down Expand Up @@ -121,6 +122,21 @@ def fake_download(nvr, target_fd):
KOJI.download_manifest_archive = orig


@pytest.fixture
def mocked_koji_get_api_version():
"""Mock global KOJI.get_api_version to return valid version"""
def fake_version():
return 1

orig = KOJI.get_api_version
try:
m = Mock(return_value=1)
KOJI.get_api_version = m
yield m
finally:
KOJI.get_api_version = orig


@pytest.fixture
def mocked_koji():
"""Return mocked KojiUtil with session"""
Expand All @@ -129,3 +145,11 @@ def mocked_koji():
ku.initialize(conf)
ku._session = flexmock()
return ku


@pytest.fixture
def mocked_quay_version():
"""Return mocked quay api version"""
with requests_mock.Mocker() as m:
m.get("/cnr/version", json={"cnr-api": "0.0.1-test"})
yield m
21 changes: 21 additions & 0 deletions tests/test_koji_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,24 @@ def test_download_manifest_archive(self, mocked_koji):
mocked_koji.download_manifest_archive('test', target_f)
target_f.seek(0)
assert target_f.read() == data

def test_get_api_version(self, mocked_koji):
"""Test get_api_version method"""

version = 1
mocked_koji.session.should_receive('getAPIVersion').and_return(version)

assert mocked_koji.get_api_version() == version

def test_get_api_version_error(self, mocked_koji):
"""Test if get_api_version method raises proper exception"""
msg = "something wrong happened"
(mocked_koji.session
.should_receive('getAPIVersion')
.and_raise(Exception(msg))
)

with pytest.raises(KojiError) as ke:
mocked_koji.get_api_version()

assert msg in str(ke)
12 changes: 11 additions & 1 deletion tests/test_quay.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
QuayPackageError,
QuayPackageNotFound,
)
from omps.quay import QuayOrganization, ReleaseVersion, OrgManager
from omps.quay import (
get_cnr_api_version,
QuayOrganization,
OrgManager,
ReleaseVersion,
)
from omps.settings import Config


Expand Down Expand Up @@ -360,3 +365,8 @@ class ConfClass:
assert isinstance(unconfigured_org, QuayOrganization)
assert not unconfigured_org.public
assert not unconfigured_org.oauth_access


def test_get_cnr_api_version(mocked_quay_version):
"""Tests of quay.get_cnr_api_version function"""
assert get_cnr_api_version() == "0.0.1-test"

0 comments on commit b98c34b

Please sign in to comment.