diff --git a/.config/constraints.txt b/.config/constraints.txt index 3e66ba0..7ce2f30 100644 --- a/.config/constraints.txt +++ b/.config/constraints.txt @@ -55,7 +55,7 @@ importlib-metadata==8.5.0 iniconfig==2.0.0 isodate==0.7.2 isort==5.13.2 -jinja2==3.1.4 +jinja2==3.1.5 jsmin==3.0.1 jsonschema==4.23.0 jsonschema-path==0.3.3 diff --git a/.config/requirements-lock.txt b/.config/requirements-lock.txt index e8d646d..8728a7f 100644 --- a/.config/requirements-lock.txt +++ b/.config/requirements-lock.txt @@ -27,7 +27,7 @@ execnet==2.1.1 filelock==3.16.1 importlib-metadata==8.5.0 iniconfig==2.0.0 -jinja2==3.1.4 +jinja2==3.1.5 jsonschema==4.23.0 jsonschema-specifications==2023.12.1 lockfile==0.12.2 diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index ccd8657..1b82bff 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -83,8 +83,9 @@ jobs: runs-on: ubuntu-24.04 needs: - tox - # if: github.ref == 'refs/heads/main' - # github.event_name == 'release' && github.event.action == 'published' + if: github.ref == 'refs/heads/main' + # This condition ensures that publishing can only happen on push events to the main branch. + # Pull request events are excluded as they don't have the necessary permissions to publish. steps: - name: Check out repository uses: actions/checkout@v4 diff --git a/src/ansible_dev_tools/resources/server/data/openapi.yaml b/src/ansible_dev_tools/resources/server/data/openapi.yaml index 1821e61..5335e87 100644 --- a/src/ansible_dev_tools/resources/server/data/openapi.yaml +++ b/src/ansible_dev_tools/resources/server/data/openapi.yaml @@ -1,9 +1,19 @@ openapi: 3.1.0 info: - title: Playbook Creator API + title: Ansible Development Tools APIs version: 1.0.0 - description: API for ansible creator + description: APIs for ansible development tools paths: + /metadata: + get: + summary: Retrieve versions of installed tools and existing API endpoints + responses: + "200": + description: A list of installed tools and their versions + content: + application/json: + schema: + $ref: "#/components/schemas/Metadata" /v1/creator/collection: post: summary: Create a new collection project @@ -95,6 +105,19 @@ paths: components: schemas: + Metadata: + type: object + properties: + versions: + type: object + additionalProperties: + type: string + apis: + type: object + additionalProperties: + type: array + items: + type: string CreatorCollection: type: object additionalProperties: false diff --git a/src/ansible_dev_tools/resources/server/server_info.py b/src/ansible_dev_tools/resources/server/server_info.py new file mode 100644 index 0000000..558bb21 --- /dev/null +++ b/src/ansible_dev_tools/resources/server/server_info.py @@ -0,0 +1,43 @@ +"""The Server Info API.""" + +from __future__ import annotations + +from django.http import HttpRequest, JsonResponse +from django.urls import get_resolver + +from ansible_dev_tools.server_utils import validate_request +from ansible_dev_tools.version_builder import version_builder + + +class GetMetadata: + """The metadata, returns the available tools with their versions and available API endpoints.""" + + def server_info(self, request: HttpRequest) -> JsonResponse: + """Return server information including versions and available APIs. + + Args: + request: HttpRequest Object + Returns: + JSON response containing tool versions and available API endpoints. + """ + validate_request(request) + versions = {} + for line in version_builder().splitlines(): + tool, version = line.split(maxsplit=1) + versions[tool] = version + + resolver = get_resolver() + urlpatterns = resolver.url_patterns + + endpoints = [str(pattern.pattern) for pattern in urlpatterns] + + grouped_endpoints: dict[str, list[str]] = {} + + for endpoint in endpoints: + parts = endpoint.split("/") + key = parts[0] + if key not in grouped_endpoints: + grouped_endpoints[key] = [] + grouped_endpoints[key].append(f"/{endpoint}") + + return JsonResponse({"versions": versions, "apis": grouped_endpoints}, status=200) diff --git a/src/ansible_dev_tools/server_utils.py b/src/ansible_dev_tools/server_utils.py index 5081f60..319de82 100644 --- a/src/ansible_dev_tools/server_utils.py +++ b/src/ansible_dev_tools/server_utils.py @@ -7,7 +7,7 @@ import yaml -from django.http import FileResponse, HttpRequest, HttpResponse +from django.http import FileResponse, HttpRequest, HttpResponse, JsonResponse from openapi_core import OpenAPI from openapi_core.contrib.django import DjangoOpenAPIRequest, DjangoOpenAPIResponse from openapi_core.exceptions import OpenAPIError @@ -26,7 +26,7 @@ ) -def validate_request(request: HttpRequest) -> RequestUnmarshalResult | HttpResponse: +def validate_request(request: HttpRequest) -> RequestUnmarshalResult | HttpResponse | JsonResponse: """Validate the request against the OpenAPI schema. Args: diff --git a/src/ansible_dev_tools/subcommands/server.py b/src/ansible_dev_tools/subcommands/server.py index 7d523a5..7ee1663 100644 --- a/src/ansible_dev_tools/subcommands/server.py +++ b/src/ansible_dev_tools/subcommands/server.py @@ -14,6 +14,7 @@ from ansible_dev_tools.resources.server.creator_v1 import CreatorFrontendV1 from ansible_dev_tools.resources.server.creator_v2 import CreatorFrontendV2 +from ansible_dev_tools.resources.server.server_info import GetMetadata if TYPE_CHECKING: @@ -21,6 +22,7 @@ urlpatterns = ( + path(route="metadata", view=GetMetadata().server_info, name="server_info"), path(route="v1/creator/playbook", view=CreatorFrontendV1().playbook), path(route="v1/creator/collection", view=CreatorFrontendV1().collection), path(route="v2/creator/playbook", view=CreatorFrontendV2().playbook), diff --git a/tests/integration/test_container.py b/tests/integration/test_container.py index 8a5aa51..a09a2a4 100644 --- a/tests/integration/test_container.py +++ b/tests/integration/test_container.py @@ -14,6 +14,7 @@ from .test_server_creator_v1 import test_collection_v1 as tst_collection_v1 from .test_server_creator_v1 import test_error as tst_error from .test_server_creator_v1 import test_playbook_v1 as tst_playbook_v1 +from .test_server_info import test_metadata as tst_get_metadata if TYPE_CHECKING: @@ -219,6 +220,16 @@ def test_playbook_v1_container(server_in_container_url: str, tmp_path: Path) -> tst_playbook_v1(server_url=server_in_container_url, tmp_path=tmp_path) +@pytest.mark.container +def test_get_metadata_container(server_in_container_url: str) -> None: + """Test the metadata endpoint. + + Args: + server_in_container_url: The dev tools server. + """ + tst_get_metadata(server_url=server_in_container_url) + + @pytest.mark.container def test_nav_collections( container_tmux: ContainerTmux, diff --git a/tests/integration/test_server_info.py b/tests/integration/test_server_info.py new file mode 100644 index 0000000..6b389c5 --- /dev/null +++ b/tests/integration/test_server_info.py @@ -0,0 +1,32 @@ +"""Test the dev tools server for metadata.""" + +from __future__ import annotations + +import requests + + +def test_metadata(server_url: str) -> None: + """Test the server info endpoint. + + Args: + server_url: The server URL. + """ + endpoint = f"{server_url}/metadata" + + response = requests.get(endpoint, timeout=10) + + expected_response_code = 200 + assert ( + response.status_code == expected_response_code + ), f"Expected status code 200 but got {response.status_code}" + + assert response.headers["Content-Type"] == "application/json" + + data = response.json() + + assert "versions" in data, "Response is missing 'versions' key" + assert "apis" in data, "Response is missing 'apis' key" + + assert len(data["versions"]) > 0, "Versions should contain at least one package" + + assert len(data["apis"]) > 0, "APIs should contain at least one endpoint" diff --git a/tools/devspaces.sh b/tools/devspaces.sh index 00c98a1..b9bc3af 100755 --- a/tools/devspaces.sh +++ b/tools/devspaces.sh @@ -30,7 +30,7 @@ mk containers check $IMAGE_NAME --engine="${ADT_CONTAINER_ENGINE}" --max-size=16 pytest --only-container --container-engine="${ADT_CONTAINER_ENGINE}" --container-name=devspaces --image-name=$IMAGE_NAME "$@" || echo "::error::Ignored failed devspaces tests, please https://github.com/ansible/ansible-dev-tools/issues/467" -if [[ -n "${GITHUB_SHA:-}" ]]; then +if [[ -n "${GITHUB_SHA:-}" && "${GITHUB_EVENT_NAME:-}" != "pull_request" ]]; then $ADT_CONTAINER_ENGINE tag $IMAGE_NAME "ghcr.io/ansible/ansible-devspaces-tmp:${GITHUB_SHA}" # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry if [[ -n "${GITHUB_TOKEN:-}" ]]; then diff --git a/tools/ee.sh b/tools/ee.sh index ed80030..91f3190 100755 --- a/tools/ee.sh +++ b/tools/ee.sh @@ -80,7 +80,7 @@ pushd docs/examples ansible-builder build popd -if [[ -n "${GITHUB_SHA:-}" ]]; then +if [[ -n "${GITHUB_SHA:-}" && "${GITHUB_EVENT_NAME:-}" != "pull_request" ]]; then FQ_IMAGE_NAME="ghcr.io/ansible/community-ansible-dev-tools-tmp:${GITHUB_SHA}-$ARCH" $ADT_CONTAINER_ENGINE tag $IMAGE_NAME "${FQ_IMAGE_NAME}" # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry diff --git a/tox.ini b/tox.ini index ba5fb24..66d1548 100644 --- a/tox.ini +++ b/tox.ini @@ -130,7 +130,7 @@ commands = [testenv:ee] description = Build the ee container image -skip_install = true +skip_install = false deps = -r .config/requirements-test.in ansible-builder