From 5f0435ba60500c6c8132ba483e7299498528e1b2 Mon Sep 17 00:00:00 2001 From: Binam Bajracharya <44302895+BinamB@users.noreply.github.com> Date: Sat, 15 Apr 2023 06:12:18 -0500 Subject: [PATCH] (PPS-261): (feat) Add service-info endpoint to DRS (#356) * intial commit * Add unit tests * Add docstring * Remove unused import * move to drs * move service-info * remove unused imports * Add more test and handle edge case * remove utils stuff * Create separate config * fix things --- .secrets.baseline | 6 +-- bin/indexd_settings.py | 4 ++ deployment/Secrets/indexd_settings.py | 4 ++ indexd/default_settings.py | 21 +++++++--- indexd/drs/blueprint.py | 45 ++++++++++++++++++++ indexd/utils.py | 21 ++++++++++ tests/default_test_settings.py | 16 +++++++- tests/test_drs.py | 59 +++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 10 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 51f4cecd..be14f3d8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -230,7 +230,7 @@ "filename": "tests/default_test_settings.py", "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_verified": false, - "line_number": 26 + "line_number": 40 } ], "tests/postgres/migrations/test_15f2e9345ade_create_tables.py": [ @@ -391,9 +391,9 @@ "filename": "tests/test_drs.py", "hashed_secret": "5666c088b494f26cd8f63ace013992f5fc391ce0", "is_verified": false, - "line_number": 31 + "line_number": 32 } ] }, - "generated_at": "2023-03-23T20:46:39Z" + "generated_at": "2023-04-13T17:00:13Z" } diff --git a/bin/indexd_settings.py b/bin/indexd_settings.py index 364aac2b..bcc67985 100644 --- a/bin/indexd_settings.py +++ b/bin/indexd_settings.py @@ -28,6 +28,10 @@ def load_json(file_name): if dist: CONFIG["DIST"] = json.loads(dist) +drs_service_info = environ.get("DRS_SERVICE_INFO", None) +if drs_service_info: + CONFIG["DRS_SERVICE_INFO"] = json.loads(drs_service_info) + CONFIG["INDEX"] = { "driver": SQLAlchemyIndexDriver( "postgresql+psycopg2://{usr}:{psw}@{pghost}:{pgport}/{db}".format( diff --git a/deployment/Secrets/indexd_settings.py b/deployment/Secrets/indexd_settings.py index 4b35c567..e7572d4f 100644 --- a/deployment/Secrets/indexd_settings.py +++ b/deployment/Secrets/indexd_settings.py @@ -29,6 +29,10 @@ def load_json(file_name): if dist: CONFIG["DIST"] = json.loads(dist) +drs_service_info = environ.get("DRS_SERVICE_INFO", None) +if drs_service_info: + CONFIG["DRS_SERVICE_INFO"] = json.loads(drs_service_info) + CONFIG["INDEX"] = { "driver": SQLAlchemyIndexDriver( "postgresql+psycopg2://{usr}:{psw}@{pghost}:{pgport}/{db}".format( diff --git a/indexd/default_settings.py b/indexd/default_settings.py index 00cbad09..f57f1054 100644 --- a/indexd/default_settings.py +++ b/indexd/default_settings.py @@ -44,14 +44,23 @@ "hints": [], "type": "dos", }, - { - "name": "DRS System", - "host": "https://example.com/api/ga4gh/drs/v1/", - "hints": [], - "type": "drs", - }, ] +CONFIG["DRS_SERVICE_INFO"] = { + "name": "DRS System", + "type": { + "group": "org.ga4gh", + "artifact": "drs", + "version": "1.0.3", + }, + "version": "1.0.3", + "id": "com.example", + "organization": { + "name": "CTDS", + "url": "http://example.com/", + }, +} + AUTH = SQLAlchemyAuthDriver("sqlite:///auth.sq3") settings = {"config": CONFIG, "auth": AUTH} diff --git a/indexd/drs/blueprint.py b/indexd/drs/blueprint.py index c8466a74..053a5045 100644 --- a/indexd/drs/blueprint.py +++ b/indexd/drs/blueprint.py @@ -1,14 +1,53 @@ +import os +import re import flask import json from indexd.errors import AuthError, AuthzError from indexd.errors import UserError from indexd.index.errors import NoRecordFound as IndexNoRecordFound from indexd.errors import IndexdUnexpectedError +from indexd.utils import reverse_url blueprint = flask.Blueprint("drs", __name__) blueprint.config = dict() blueprint.index_driver = None +blueprint.service_info = {} + + +@blueprint.route("/ga4gh/drs/v1/service-info", methods=["GET"]) +def get_drs_service_info(): + """ + Returns DRS compliant service information + """ + + reverse_domain_name = reverse_url(url=os.environ["HOSTNAME"]) + + ret = { + "id": reverse_domain_name, + "name": "DRS System", + "version": "1.0.3", + "type": { + "group": "org.ga4gh", + "artifact": "drs", + "version": "1.0.3", + }, + "organization": { + "name": "CTDS", + "url": "https://" + os.environ["HOSTNAME"], + }, + } + + if blueprint.service_info: + for key, value in blueprint.service_info.items(): + if key in ret: + if isinstance(value, dict): + for inner_key, inner_value in value.items(): + ret[key][inner_key] = inner_value + else: + ret[key] = value + + return flask.jsonify(ret), 200 @blueprint.route("/ga4gh/drs/v1/objects/", methods=["GET"]) @@ -329,3 +368,9 @@ def handle_unexpected_error(err): def get_config(setup_state): index_config = setup_state.app.config["INDEX"] blueprint.index_driver = index_config["driver"] + + +@blueprint.record +def get_config(setup_state): + if "DRS_SERVICE_INFO" in setup_state.app.config: + blueprint.service_info = setup_state.app.config["DRS_SERVICE_INFO"] diff --git a/indexd/utils.py b/indexd/utils.py index bd36ca09..29a467ab 100644 --- a/indexd/utils.py +++ b/indexd/utils.py @@ -1,5 +1,6 @@ import logging import re +from urllib.parse import urlparse def hint_match(record, hints): @@ -189,3 +190,23 @@ def migrate_database(driver, migrate_functions, current_schema_version, model): f(engine=driver.engine, session=s) schema_version.version += 1 s.add(schema_version) + + +def reverse_url(url): + """ + Reverse the domain name for drs service-info IDs + Args: + url (str): url of the domain + example: drs.example.org + + returns: + id (str): DRS service-info ID + example: org.example.drs + """ + parsed_url = urlparse(url) + if parsed_url.scheme in ["http", "https"]: + url = parsed_url.hostname + segments = url.split(".") + reversed_segments = reversed(segments) + res = ".".join(reversed_segments) + return res diff --git a/tests/default_test_settings.py b/tests/default_test_settings.py index fe1cedd4..8dee48ea 100644 --- a/tests/default_test_settings.py +++ b/tests/default_test_settings.py @@ -9,9 +9,23 @@ "host": "https://fictitious-commons.io/index/", "hints": [".*dg\\.4503.*"], "type": "indexd", - } + }, ] +CONFIG["DRS_SERVICE_INFO"] = { + "name": "DRS System", + "type": { + "group": "org.ga4gh", + "artifact": "drs", + "version": "1.0.3", + }, + "version": "1.0.3", + "organization": { + "name": "CTDS", + "url": "https://fictitious-commons.io", + }, +} + os.environ["PRESIGNED_FENCE_URL"] = "https://fictitious-commons.io/" os.environ["HOSTNAME"] = "fictitious-commons.io" settings = {"config": CONFIG, "auth": AUTH} diff --git a/tests/test_drs.py b/tests/test_drs.py index 673a56fc..45454688 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -1,3 +1,4 @@ +import flask import json import tests.conftest import requests @@ -155,3 +156,61 @@ def test_get_drs_with_encoded_slash(client, user): assert rec_2["checksums"][0]["type"] == k assert rec_2["version"] assert rec_2["self_uri"] == "drs://testprefix:" + rec_1["did"].split(":")[1] + + +def test_drs_service_info_endpoint(client): + """ + Test drs service endpoint with drs service info friendly distribution information + """ + app = flask.Flask(__name__) + + expected_info = { + "id": "io.fictitious-commons", + "name": "DRS System", + "type": { + "group": "org.ga4gh", + "artifact": "drs", + "version": "1.0.3", + }, + "version": "1.0.3", + "organization": { + "name": "CTDS", + "url": "https://fictitious-commons.io", + }, + } + + res = client.get("/ga4gh/drs/v1/service-info") + + assert res.status_code == 200 + assert res.json == expected_info + + +def test_drs_service_info_no_information_configured(client): + """ + Test drs service info endpoint when dist is not configured in the indexd config file + """ + expected_info = { + "id": "io.fictitious-commons", + "name": "DRS System", + "type": { + "group": "org.ga4gh", + "artifact": "drs", + "version": "1.0.3", + }, + "version": "1.0.3", + "organization": { + "name": "CTDS", + "url": "https://fictitious-commons.io", + }, + } + backup = settings["config"]["DRS_SERVICE_INFO"].copy() + + try: + settings["config"]["DRS_SERVICE_INFO"].clear() + + res = client.get("/ga4gh/drs/v1/service-info") + + assert res.status_code == 200 + assert res.json == expected_info + finally: + settings["config"]["DRS_SERVICE_INFO"] = backup