diff --git a/docs/deployment.md b/docs/deployment.md index 39836644a..7852f121a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -123,3 +123,5 @@ Minikube is the recommended method for macOS users **Note**: For fedora, the binary may be `go-task`. You can now access the UI at using the above credentials. +Also you can inspect the API documentation at +or navigate through the resources with the DRF browsable API at diff --git a/src/aap_eda/api/urls.py b/src/aap_eda/api/urls.py index 7f80fa23e..1d5f5f1c4 100644 --- a/src/aap_eda/api/urls.py +++ b/src/aap_eda/api/urls.py @@ -79,22 +79,35 @@ ), ] -v1_urls = [ - path("status/", core_views.StatusView.as_view()), - path("", include(dab_urls)), - path("", include(resource_api_urls)), +eda_v1_urls = [ + path("status/", core_views.StatusView.as_view(), name="status"), path("", include(openapi_urls)), - path("auth/session/login/", views.SessionLoginView.as_view()), - path("auth/session/logout/", views.SessionLogoutView.as_view()), + path( + "auth/session/login/", + views.SessionLoginView.as_view(), + name="session-login", + ), + path( + "auth/session/logout/", + views.SessionLogoutView.as_view(), + name="session-logout", + ), path( "auth/token/refresh/", jwt_views.TokenRefreshView.as_view(), - name="token_refresh", + name="token-refresh", ), - path("users/me/", views.CurrentUserView.as_view()), + path("users/me/", views.CurrentUserView.as_view(), name="current-user"), *router.urls, ] +dab_urls = [ + path("", include(dab_urls)), + path("", include(resource_api_urls)), +] + +v1_urls = eda_v1_urls + dab_urls urlpatterns = [ + path("v1/", views.ApiV1RootView.as_view()), path("v1/", include(v1_urls)), ] diff --git a/src/aap_eda/api/views/__init__.py b/src/aap_eda/api/views/__init__.py index 0c5edd6c8..8d94c5376 100644 --- a/src/aap_eda/api/views/__init__.py +++ b/src/aap_eda/api/views/__init__.py @@ -20,6 +20,7 @@ from .event_stream import EventStreamViewSet from .organization import OrganizationViewSet from .project import ProjectViewSet +from .root import ApiV1RootView from .rulebook import AuditRuleViewSet, RulebookViewSet from .team import TeamViewSet from .user import CurrentUserAwxTokenViewSet, CurrentUserView, UserViewSet @@ -28,7 +29,6 @@ # auth "SessionLoginView", "SessionLogoutView", - "RoleViewSet", # project "ProjectViewSet", "AuditRuleViewSet", @@ -51,4 +51,6 @@ "OrganizationViewSet", # teams "TeamViewSet", + # root + "ApiV1RootView", ) diff --git a/src/aap_eda/api/views/root.py b/src/aap_eda/api/views/root.py new file mode 100644 index 000000000..3bae460ad --- /dev/null +++ b/src/aap_eda/api/views/root.py @@ -0,0 +1,59 @@ +# Copyright 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 logging + +from django.conf import settings +from django.urls import URLPattern, URLResolver +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.views import APIView + +LOGGER = logging.getLogger(__name__) + + +class ApiV1RootView(APIView): + def get(self, request, *args, **kwargs): + urls = get_api_v1_urls(request=request) + return Response(urls) + + +def get_api_v1_urls(request=None): + from aap_eda.api import urls + + def list_urls(urls): + url_list = {} + for url in urls: + if isinstance(url, URLResolver): + url_list.update(list_urls(url.url_patterns)) + elif isinstance(url, URLPattern): + name = url.name + if not name: + LOGGER.warning( + "URL %s has no name, DRF browsable API will omit it", + url.pattern, + ) + continue + if url.pattern.regex.groups: + continue + url_list[name] = reverse(name, request=request) + return url_list + + if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: + urls = urls.v1_urls + else: + urls = urls.eda_v1_urls + + return list_urls(urls) diff --git a/src/aap_eda/settings/default.py b/src/aap_eda/settings/default.py index 538fff526..1eb1858d8 100644 --- a/src/aap_eda/settings/default.py +++ b/src/aap_eda/settings/default.py @@ -301,7 +301,8 @@ def _get_databases_settings() -> dict: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = "static/" +STATIC_URL = settings.get("STATIC_URL", "static/") +STATIC_ROOT = settings.get("STATIC_ROOT", "/var/lib/eda/static") MEDIA_ROOT = settings.get("MEDIA_ROOT", "/var/lib/eda/files") diff --git a/tests/integration/api/test_v1_root.py b/tests/integration/api/test_v1_root.py new file mode 100644 index 000000000..182a572e2 --- /dev/null +++ b/tests/integration/api/test_v1_root.py @@ -0,0 +1,99 @@ +# Copyright 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 pytest + +from tests.integration.constants import api_url_v1 + + +@pytest.mark.parametrize( + "expected_slugs,use_shared_resource", + [ + pytest.param( + [ + "/status/", + "/openapi.json", + "/openapi.yaml", + "/docs", + "/redoc", + "/auth/session/login/", + "/auth/session/logout/", + "/auth/token/refresh/", + "/users/me/", + "/projects/", + "/rulebooks/", + "/activations/", + "/activation-instances/", + "/audit-rules/", + "/users/", + "/event-streams/", + "/users/me/awx-tokens/", + "/credential-types/", + "/eda-credentials/", + "/decision-environments/", + "/organizations/", + "/teams/", + ], + True, + id="with_shared_resource", + ), + pytest.param( + [ + "/status/", + "/role_definitions/", + "/role_user_assignments/", + "/role_team_assignments/", + "/role_metadata/", + "/service-index/metadata/", + "/service-index/validate-local-account/", + "/service-index/resources/", + "/service-index/resource-types/", + "/service-index/", + "/openapi.json", + "/openapi.yaml", + "/docs", + "/redoc", + "/auth/session/login/", + "/auth/session/logout/", + "/auth/token/refresh/", + "/users/me/", + "/projects/", + "/rulebooks/", + "/activations/", + "/activation-instances/", + "/audit-rules/", + "/users/", + "/event-streams/", + "/users/me/awx-tokens/", + "/credential-types/", + "/eda-credentials/", + "/decision-environments/", + "/organizations/", + "/teams/", + ], + False, + id="no_shared_resource", + ), + ], +) +@pytest.mark.django_db +def test_v1_root(admin_client, request, expected_slugs, use_shared_resource): + if use_shared_resource: + request.getfixturevalue("use_shared_resource_setting") + response = admin_client.get(f"{api_url_v1}/") + assert response.status_code == 200 + assert len(response.data) == len(expected_slugs) + for slug in expected_slugs: + assert any( + slug in url for url in response.data.values() + ), f"Expected slug {slug} not found in response" diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index b49d546e5..939f8369e 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -1,6 +1,7 @@ FROM quay.io/centos/centos:stream9 ARG USER_ID=${USER_ID:-1001} +ARG STATIC_ROOT=/var/lib/eda/static RUN useradd --uid "$USER_ID" --gid 0 --home-dir /app --create-home eda \ && mkdir -p /app/.local /var/lib/eda/ \ && chown -R "${USER_ID}:0" /app/.local /var/lib/eda \ @@ -52,6 +53,7 @@ RUN poetry install -E all --no-root --no-cache \ COPY . $SOURCES_DIR/ RUN poetry install -E all --only-root RUN pip install ansible-core +RUN EDA_SECRET_KEY=dummy EDA_STATIC_ROOT=${STATIC_ROOT} aap-eda-manage collectstatic --noinput --clear USER 0 RUN for dir in \