diff --git a/dci/api/v2/__init__.py b/dci/api/v2/__init__.py new file mode 100644 index 000000000..120ea5643 --- /dev/null +++ b/dci/api/v2/__init__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Red Hat, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import flask +from flask import json + +import logging + +logger = logging.getLogger(__name__) + +api = flask.Blueprint("api_v2", __name__) + + +@api.route("/", strict_slashes=False) +def index(): + logger.info("control server is ok...") + return flask.Response( + json.dumps({"_status": "OK", "message": "Distributed CI.", "version": "2"}), + status=200, + content_type="application/json", + ) + + +import dci.api.v2.components # noqa diff --git a/dci/api/v2/components.py b/dci/api/v2/components.py new file mode 100644 index 000000000..32841e925 --- /dev/null +++ b/dci/api/v2/components.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Red Hat, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +import flask +import logging +import requests + +from dci.api.v2 import api +from dci.api.v1 import base +from dci.api.v1.components import _verify_component_and_topic_access +from dci import decorators +from dci.common import exceptions as dci_exc +from dci.dci_config import CONFIG +from dci.db import models2 + + +logger = logging.getLogger(__name__) + + +@api.route("/components//files/", methods=["GET", "HEAD"]) +@decorators.login_required +def get_component_file_from_rhdl(user, c_id, filepath): + component = base.get_resource_orm(models2.Component, c_id) + _verify_component_and_topic_access(user, component) + + if filepath == "dci_files_list.json": + filepath = "rhdl_files_list.json" + normalized_filepath = os.path.normpath("/" + filepath).lstrip("/") + normalized_rhdl_component_filepath = os.path.join( + component.display_name, "files", normalized_filepath + ) + rhdl_component_filepath = os.path.join(component.display_name, "files", filepath) + if rhdl_component_filepath != normalized_rhdl_component_filepath: + raise dci_exc.DCIException("Request malformed: filepath is invalid") + + rhdl_file_url = os.path.join( + CONFIG["RHDL_API_URL"], "components", normalized_rhdl_component_filepath + ) + + redirect = requests.get( + rhdl_file_url, allow_redirects=False, timeout=CONFIG["REQUESTS_TIMEOUT"] + ) + if redirect.status_code != 302: + raise dci_exc.DCIException( + message=redirect.content, status_code=redirect.status_code + ) + + return flask.Response(None, 302, headers={"Location": redirect.headers["Location"]}) diff --git a/dci/app.py b/dci/app.py index 9f4560334..7b721086b 100644 --- a/dci/app.py +++ b/dci/app.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2015-2016 Red Hat, Inc +# Copyright (C) 2015-2024 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. from dci.api import v1 as api_v1 +from dci.api import v2 as api_v2 from dci.common import exceptions from dci.common import utils from dci.db import models2 @@ -167,6 +168,7 @@ def teardown_request(_): # Registering REST API v1 dci_app.register_blueprint(api_v1.api, url_prefix="/api/v1") + dci_app.register_blueprint(api_v2.api, url_prefix="/api/v2") # Registering custom encoder dci_app.json_encoder = utils.JSONEncoder diff --git a/dci/settings.py b/dci/settings.py index 365c867da..5b2c0fde9 100644 --- a/dci/settings.py +++ b/dci/settings.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- # -# Copyright 2015-2016 Red Hat, Inc. +# Copyright 2015-2024 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -115,3 +115,7 @@ SSO_REALM = os.getenv("SSO_REALM", "redhat-external") CERTIFICATION_URL = "https://access.stage.redhat.com/hydra/rest/cwe/xmlrpc/v2" + +RHDL_API_URL = "https://rhdl.distributed-ci.io/api/v1" + +REQUESTS_TIMEOUT = (3.0, 10.0) diff --git a/test-requirements.txt b/test-requirements.txt index 82005664f..6dcc00029 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,4 +2,5 @@ mock pytest dciclient flake8 -tox \ No newline at end of file +tox +responses diff --git a/tests/api/v2/test_components.py b/tests/api/v2/test_components.py new file mode 100644 index 000000000..87d2cac5c --- /dev/null +++ b/tests/api/v2/test_components.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 Red Hat, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import responses + +from dci import dci_config + + +@responses.activate +def test_get_component_file_from_rhdl_user_team_in_RHEL_with_released_component( + admin, + remoteci_context, + remoteci_user, + rhel_product, + rhel_80_component, +): + rhdl_api_url = dci_config.CONFIG["RHDL_API_URL"] + rhdl_composeinfo_url = ( + f"{rhdl_api_url}/components/{rhel_80_component['name']}/files/.composeinfo" + ) + + responses.add( + method=responses.GET, + url=rhdl_composeinfo_url, + status=302, + headers={ + "Location": "https://wedontcare", + }, + ) + responses.add( + method=responses.HEAD, + url=rhdl_composeinfo_url, + status=302, + headers={ + "Location": "https://wedontcare", + }, + ) + + r = remoteci_context.get( + f"/api/v2/components/{rhel_80_component['id']}/files/.composeinfo" + ) + assert r.status_code == 302 + assert r.headers["Location"] is not None + assert responses.assert_call_count(rhdl_composeinfo_url, 1) is True + + r = remoteci_context.head( + f"/api/v2/components/{rhel_80_component['id']}/files/.composeinfo" + ) + assert r.status_code == 302 + assert r.headers["Location"] is not None + + assert responses.assert_call_count(rhdl_composeinfo_url, 2) is True + + # delete product team permission + r = admin.delete( + "/api/v1/products/%s/teams/%s" % (rhel_product["id"], remoteci_user["team_id"]), + ) + assert r.status_code == 204 + + r = remoteci_context.get( + "/api/v1/components/%s/files/.composeinfo" % rhel_80_component["id"] + ) + assert r.status_code == 401 + + r = remoteci_context.head( + "/api/v1/components/%s/files/.composeinfo" % rhel_80_component["id"] + ) + assert r.status_code == 401 + + +@responses.activate +def test_get_files_list_from_rhdl_renames_files_list( + remoteci_context, + rhel_80_component, +): + rhdl_api_url = dci_config.CONFIG["RHDL_API_URL"] + rhdl_files_list_url = f"{rhdl_api_url}/components/{rhel_80_component['name']}/files/rhdl_files_list.json" + responses.add( + method=responses.GET, + url=rhdl_files_list_url, + status=302, + headers={ + "Location": "https://wedontcare", + }, + ) + + r = remoteci_context.get( + f"/api/v2/components/{rhel_80_component['id']}/files/dci_files_list.json" + ) + assert r.status_code == 302 + assert r.headers["Location"] is not None + assert responses.assert_call_count(rhdl_files_list_url, 1) is True