Skip to content

Commit

Permalink
Merge pull request #4 from bento-platform/features/service-registry-v…
Browse files Browse the repository at this point in the history
…ersion

Features/service registry version
  • Loading branch information
noctillion authored Oct 4, 2022
2 parents f7e33bb + e9a4789 commit d69429b
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 58 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ htmlcov/
.idea/workspace.xml
.idea/dataSources*
/chord_services.json
.vscode
145 changes: 101 additions & 44 deletions bento_service_registry/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import requests
import sys
import subprocess

from bento_lib.responses.flask_errors import (
flask_error_wrap,
Expand All @@ -22,6 +23,7 @@


application = Flask(__name__)

application.config.from_mapping(
BENTO_DEBUG=os.environ.get("CHORD_DEBUG", os.environ.get("FLASK_ENV", "production")).strip().lower() in (
"true", "1", "development"),
Expand All @@ -32,6 +34,8 @@
URL_PATH_FORMAT=os.environ.get("URL_PATH_FORMAT", "api/{artifact}"),
)

path_for_git = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

# Generic catch-all
application.register_error_handler(Exception, flask_error_wrap_with_traceback(flask_internal_server_error,
sr_compat=True,
Expand All @@ -44,6 +48,20 @@ def get_service_url(artifact: str):
return urljoin(current_app.config["CHORD_URL"], current_app.config["URL_PATH_FORMAT"].format(artifact=artifact))


def get_chord_services():
"""
Reads the list of services from the chord_services.json file
"""
services = []
try:
with open(current_app.config["CHORD_SERVICES"], "r") as f:
services = [s for s in json.load(f) if not s.get("disabled")] # Skip disabled services
except Exception as e:
except_name = type(e).__name__
print("Error in retrieving services information from json file.", except_name)
return services


with application.app_context():
SERVICE_ID = current_app.config["SERVICE_ID"]
SERVICE_INFO = {
Expand All @@ -56,77 +74,93 @@ def get_service_url(artifact: str):
"url": "http://www.computationalgenomics.ca"
},
"contactUrl": "mailto:[email protected]",
"version": bento_service_registry.__version__
"version": bento_service_registry.__version__,
"url": get_service_url(SERVICE_ARTIFACT),
"environment": "prod"
}

with open(current_app.config["CHORD_SERVICES"], "r") as f:
CHORD_SERVICES = [s for s in json.load(f) if not s.get("disabled")] # Skip disabled services

service_info_cache = {
# Pre-populate service-info cache with data for the current service
SERVICE_ARTIFACT: {**SERVICE_INFO, "url": get_service_url(SERVICE_ARTIFACT)},
}
chord_services_content = get_chord_services() # Not a constant: can change when a service is updated


def get_service(service_artifact):
s_url = get_service_url(service_artifact)
# special case: requesting info about the current service. Avoids request timeout
# when running gunicorn on a single worker
if service_artifact == SERVICE_ARTIFACT:
return _service_info()

if service_artifact not in service_info_cache:
service_info_url = urljoin(f"{s_url}/", "service-info")
s_url = get_service_url(service_artifact)
service_info_url = urljoin(f"{s_url}/", "service-info")

print(f"[{SERVICE_NAME}] Contacting {service_info_url}", flush=True)
print(f"[{SERVICE_NAME}] Contacting {service_info_url}", flush=True)

# Optional Authorization HTTP header to forward to nested requests
# TODO: Move X-Auth... constant to bento_lib
auth_header = request.headers.get("X-Authorization", request.headers.get("Authorization"))
# Optional Authorization HTTP header to forward to nested requests
# TODO: Move X-Auth... constant to bento_lib
auth_header = request.headers.get("X-Authorization", request.headers.get("Authorization"))

try:
r = requests.get(
service_info_url,
headers={"Authorization": auth_header} if auth_header else {},
timeout=current_app.config["CONTACT_TIMEOUT"],
verify=not current_app.config["BENTO_DEBUG"],
)
service_resp = {}

if r.status_code != 200:
print(f"[{SERVICE_NAME}] Non-200 status code on {service_artifact}: {r.status_code}\n"
f" Content: {r.text}", file=sys.stderr, flush=True)
try:
r = requests.get(
service_info_url,
headers={"Authorization": auth_header} if auth_header else {},
timeout=current_app.config["CONTACT_TIMEOUT"],
verify=not current_app.config["BENTO_DEBUG"],
)

# If we have the special case where we got a JWT error from the proxy script, we can safely print out
# headers for debugging, since the JWT leaked isn't valid anyway.
if "invalid jwt" in r.text:
print(f" Encountered auth error, tried to use header: {auth_header}",
file=sys.stderr, flush=True)
if r.status_code != 200:
print(f"[{SERVICE_NAME}] Non-200 status code on {service_artifact}: {r.status_code}\n"
f" Content: {r.text}", file=sys.stderr, flush=True)

return None
# If we have the special case where we got a JWT error from the proxy script, we can safely print out
# headers for debugging, since the JWT leaked isn't valid anyway.
if "invalid jwt" in r.text:
print(f" Encountered auth error, tried to use header: {auth_header}",
file=sys.stderr, flush=True)

except requests.exceptions.Timeout:
print(f"[{SERVICE_NAME}] Encountered timeout with {service_info_url}", file=sys.stderr, flush=True)
return None

try:
service_info_cache[service_artifact] = {**r.json(), "url": s_url}
except JSONDecodeError:
print(f"[{SERVICE_NAME}] Encountered invalid response from {service_info_url}: {r.text}")
return None
except requests.exceptions.Timeout:
print(f"[{SERVICE_NAME}] Encountered timeout with {service_info_url}", file=sys.stderr, flush=True)
return None

try:
service_resp[service_artifact] = {**r.json(), "url": s_url}
except JSONDecodeError:
print(f"[{SERVICE_NAME}] Encountered invalid response from {service_info_url}: {r.text}")
return None

return service_info_cache[service_artifact]
return service_resp[service_artifact]


@application.before_first_request
def before_first_request_func():
try:
subprocess.run(["git", "config", "--global", "--add", "safe.directory", str(path_for_git)])
except Exception as e:
except_name = type(e).__name__
print("Error in dev-mode retrieving git folder configuration", except_name)


@application.route("/bento-services")
@application.route("/chord-services")
def chord_services():
return jsonify(CHORD_SERVICES)
return jsonify(get_chord_services())


def _services():
return [s for s in (
get_service(s["type"]["artifact"]) for s in chord_services_content
) if s is not None]


@application.route("/services")
def services():
return jsonify([s for s in (get_service(s["type"]["artifact"]) for s in CHORD_SERVICES) if s is not None])
return jsonify(_services())


@application.route("/services/<string:service_id>")
def service_by_id(service_id):
services_by_id = {s["id"]: s for s in service_info_cache.values()}
services_by_id = {s["id"]: s for s in _services()}
if service_id not in services_by_id:
return flask_not_found_error(f"Service with ID {service_id} was not found in registry")

Expand All @@ -136,10 +170,33 @@ def service_by_id(service_id):

@application.route("/services/types")
def service_types():
return jsonify(sorted(set(s["type"] for s in service_info_cache.values())))
return jsonify(sorted(set(s["type"] for s in _services())))


def _service_info():
if not current_app.config["BENTO_DEBUG"]:
return SERVICE_INFO

info = {
**SERVICE_INFO,
"environment": "dev"
}
try:
res_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"])
if res_tag:
info["git_tag"] = res_tag.decode().rstrip()
res_branch = subprocess.check_output(["git", "branch", "--show-current"])
if res_branch:
info["git_branch"] = res_branch.decode().rstrip()

except Exception as e:
except_name = type(e).__name__
print("Error in dev-mode retrieving git information", except_name)

return info # updated service info with the git info


@application.route("/service-info")
def service_info():
# Spec: https://github.com/ga4gh-discovery/ga4gh-service-info
return jsonify(SERVICE_INFO)
return jsonify(_service_info())
2 changes: 1 addition & 1 deletion bento_service_registry/package.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = bento_service_registry
version = 0.5.1
version = 0.6.0
authors = David Lougheed
author_emails = [email protected]
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
python_requires=">=3.6",
install_requires=[
"bento_lib[flask]==2.2.1",
"Flask>=1.1.2,<2.0",
"Flask>=1.1.2,<2.0",
"requests>=2.25.1,<3.0",
],

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
def _setup_env():
os.environ["CHORD_SERVICES"] = os.path.join(os.path.dirname(__file__), "chord_services.json")
os.environ["URL_PATH_FORMAT"] = "" # Mount off of root URL for testing
os.environ["CHORD_DEBUG"] = ""


@pytest.fixture()
Expand Down
15 changes: 3 additions & 12 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import json


def test_service_info(client, service_info):
r = client.get("/service-info")
d = r.get_json()
# TODO: Check against service-info schema
assert r.status_code == 200
assert json.dumps(d, sort_keys=True) == json.dumps(service_info, sort_keys=True)
assert d == service_info


def test_chord_service_list(client):
Expand All @@ -22,20 +19,14 @@ def test_service_list(client, service_info):
d = r.get_json()
assert r.status_code == 200
assert len(d) == 1
assert json.dumps(d[0], sort_keys=True) == json.dumps({
**service_info,
"url": "http://127.0.0.1:5000/"
}, sort_keys=True)
assert d[0] == service_info


def test_service_detail(client, service_info):
r = client.get(f"/services/{service_info['id']}")
d = r.get_json()
assert r.status_code == 200
assert json.dumps(d, sort_keys=True) == json.dumps({
**service_info,
"url": "http://127.0.0.1:5000/"
}, sort_keys=True)
assert d == service_info

r = client.get("/services/does-not-exist")
assert r.status_code == 404
Expand Down

0 comments on commit d69429b

Please sign in to comment.