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 \