diff --git a/poetry.lock b/poetry.lock index 17c2a383b..a1482b7ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -919,7 +919,7 @@ bcrypt = ["bcrypt"] [[package]] name = "django-ansible-base" -version = "2025.10.20" +version = "2025.11.24.0.dev6+g9db723788" description = "A Django app used by ansible services" optional = false python-versions = ">=3.10" @@ -931,7 +931,7 @@ develop = false asgiref = {version = "*", optional = true, markers = "extra == \"resource-registry\""} channels = {version = "*", optional = true, markers = "extra == \"channel-auth\""} cryptography = "*" -Django = ">=4.2.21,<6.0" +Django = ">=4.2.26,<6.0" django-crum = "*" django-flags = {version = "*", optional = true, markers = "extra == \"feature-flags\""} django-redis = {version = "*", optional = true, markers = "extra == \"redis-client\""} @@ -959,8 +959,8 @@ testing = ["cryptography", "pytest", "pytest-django"] [package.source] type = "git" url = "https://github.com/ansible/django-ansible-base.git" -reference = "2025.10.20" -resolved_reference = "93579cf5d9e70c92c39bc60cb30060df852b3ddf" +reference = "devel" +resolved_reference = "9db7237883e071724eb927b4dc56966d0ec28106" [[package]] name = "django-crum" @@ -1972,14 +1972,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "23.1" +version = "25.0" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main", "lint", "test"] files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] @@ -3535,4 +3535,4 @@ dev = ["psycopg-binary"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "ef5e901f52b89b4a7f06b98312d236cae464df5c44cd95c1d25f8973cd2dc263" +content-hash = "d81fafb8cac4da61e983cce1e499820fe9a01a93ae1582b4fd4137ec6bd243d3" diff --git a/pyproject.toml b/pyproject.toml index 6bf3de210..266a352a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ cryptography = ">=42,<43" kubernetes = "26.1.*" podman = "5.4.*" rq-scheduler = "^0.10" -django-ansible-base = { git = "https://github.com/ansible/django-ansible-base.git", tag = "2025.10.20", extras = [ +django-ansible-base = { git = "https://github.com/ansible/django-ansible-base.git", branch = "devel", extras = [ "channel-auth", "rbac", "redis-client", @@ -85,6 +85,7 @@ awx-plugins-core = { version = "^0.0.1a10", extras = [ "credentials-thycotic-dsv", "credentials-thycotic-tss" ]} +packaging = "^25.0" [tool.poetry.group.test.dependencies] diff --git a/src/aap_eda/api/resource_api.py b/src/aap_eda/api/resource_api.py index cef28a07f..e2c80fcd0 100644 --- a/src/aap_eda/api/resource_api.py +++ b/src/aap_eda/api/resource_api.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ansible_base.feature_flags.models import AAPFlag + # RBAC models from ansible_base.rbac.models import RoleDefinition from ansible_base.resource_registry.registry import ( @@ -21,6 +23,7 @@ SharedResource, ) from ansible_base.resource_registry.shared_types import ( + FeatureFlagType, OrganizationType, RoleDefinitionType, TeamType, @@ -61,4 +64,10 @@ class APIConfig(ServiceAPIConfig): serializer=RoleDefinitionType, is_provider=False ), ), + ResourceConfig( + AAPFlag, + shared_resource=SharedResource( + serializer=FeatureFlagType, is_provider=False + ), + ), ) diff --git a/src/aap_eda/settings/core.py b/src/aap_eda/settings/core.py index 094151e6e..2d3e1a0d4 100644 --- a/src/aap_eda/settings/core.py +++ b/src/aap_eda/settings/core.py @@ -19,24 +19,9 @@ DISPATCHERD_FEATURE_FLAG_NAME = "FEATURE_DISPATCHERD_ENABLED" ANALYTICS_FEATURE_FLAG_NAME = "FEATURE_EDA_ANALYTICS_ENABLED" -FLAGS = { - ANALYTICS_FEATURE_FLAG_NAME: [ - { - "condition": "boolean", - "value": False, - }, - ], - DISPATCHERD_FEATURE_FLAG_NAME: [ - { - "condition": "boolean", - "value": False, - }, - ], -} INSTALLED_APPS = [ "daphne", - "flags", # Django apps "django.contrib.auth", "django.contrib.contenttypes", diff --git a/src/aap_eda/settings/default.py b/src/aap_eda/settings/default.py index 4ffb3e463..63f61ced3 100644 --- a/src/aap_eda/settings/default.py +++ b/src/aap_eda/settings/default.py @@ -19,7 +19,6 @@ load_dab_settings, load_envvars, load_standard_settings_files, - toggle_feature_flags, ) from .post_load import post_loading @@ -52,13 +51,4 @@ post_loading(DYNACONF) load_dab_settings(DYNACONF) -# toggle feature flags, considering flags coming from -# /etc/ansible-automation-platform/*.yaml -# and envvars like `EDA_FEATURE_FOO_ENABLED=true -DYNACONF.update( - toggle_feature_flags(DYNACONF), - loader_identifier="settings:toggle_feature_flags", - merge=True, -) - export(__name__, DYNACONF) # export back to django.conf.settings diff --git a/tests/conftest.py b/tests/conftest.py index 611c2926c..d6a293aa1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,9 @@ import logging import pytest +from ansible_base.feature_flags.utils import ( + create_initial_data as seed_feature_flags, +) from django.conf import settings from aap_eda.core import enums, models @@ -91,6 +94,11 @@ def aap_credential_type(preseed_credential_types): ) +@pytest.fixture +def preseed_feature_flags(): + seed_feature_flags() + + ################################################################# # Redis ################################################################# diff --git a/tests/integration/api/test_feature_flags.py b/tests/integration/api/test_feature_flags.py index da6f540e7..fca99c634 100644 --- a/tests/integration/api/test_feature_flags.py +++ b/tests/integration/api/test_feature_flags.py @@ -1,41 +1,31 @@ import pytest -from ansible_base.lib.dynamic_config import toggle_feature_flags +from ansible_base.feature_flags.models import AAPFlag +from ansible_base.feature_flags.utils import ( + create_initial_data as seed_feature_flags, +) from django.conf import settings -from django.test import override_settings -from flags.state import flag_state +from flags.state import flag_state, get_flags from rest_framework import status from tests.integration.constants import api_url_v1 @pytest.mark.django_db -def test_feature_flags_list_endpoint(admin_client): +def test_feature_flags_list_endpoint(admin_client, preseed_feature_flags): response = admin_client.get(f"{api_url_v1}/feature_flags_state/") assert response.status_code == status.HTTP_200_OK, response.data # Validates expected default feature flags # Modify each time a flag is added to default settings - assert len(response.data) == 2 + assert len(response.data) == len(get_flags()) assert response.data[settings.ANALYTICS_FEATURE_FLAG_NAME] is False assert response.data[settings.DISPATCHERD_FEATURE_FLAG_NAME] is False -@override_settings( - FLAGS={ - "FEATURE_SOME_PLATFORM_FLAG_ENABLED": [ - {"condition": "boolean", "value": False}, - ], - }, - FEATURE_SOME_PLATFORM_FLAG_ENABLED=True, -) +@pytest.mark.parametrize("flag_value", [True, False]) @pytest.mark.django_db -def test_feature_flags_toggle(): - settings_override = { - "FLAGS": settings.FLAGS, - "FEATURE_SOME_PLATFORM_FLAG_ENABLED": settings.FEATURE_SOME_PLATFORM_FLAG_ENABLED, # noqa: E501 - } - assert toggle_feature_flags(settings_override) == { - "FLAGS__FEATURE_SOME_PLATFORM_FLAG_ENABLED": [ - {"condition": "boolean", "value": True}, - ] - } - assert flag_state("FEATURE_SOME_PLATFORM_FLAG_ENABLED") is True +def test_feature_flags_toggle(flag_value): + flag_name = "FEATURE_EDA_ANALYTICS_ENABLED" + setattr(settings, flag_name, flag_value) + AAPFlag.objects.all().delete() + seed_feature_flags() + assert flag_state(flag_name) is flag_value diff --git a/tests/integration/api/test_root.py b/tests/integration/api/test_root.py index 08ff5806c..187fd4218 100644 --- a/tests/integration/api/test_root.py +++ b/tests/integration/api/test_root.py @@ -92,6 +92,9 @@ "/organizations/", "/teams/", "/event-streams/", + "feature_flags/states/", + # To be removed after all components + # have migrated away from this endpoint "/feature_flags_state/", "/credential-input-sources/", ], diff --git a/tests/unit/test_features.py b/tests/unit/test_features.py index 742277764..d1ac8b4e2 100644 --- a/tests/unit/test_features.py +++ b/tests/unit/test_features.py @@ -15,6 +15,10 @@ """Unit tests for feature flags functionality.""" import pytest +from ansible_base.feature_flags.models import AAPFlag +from ansible_base.feature_flags.utils import ( + create_initial_data as seed_feature_flags, +) from aap_eda.settings import features from aap_eda.settings.features import _get_feature @@ -29,14 +33,10 @@ def clear_feature_cache(): @pytest.mark.django_db def test_get_feature_flag(settings): """Test getting feature flag values.""" - settings.FLAGS = { - settings.DISPATCHERD_FEATURE_FLAG_NAME: [ - {"condition": "boolean", "value": True} - ], - settings.ANALYTICS_FEATURE_FLAG_NAME: [ - {"condition": "boolean", "value": False} - ], - } + AAPFlag.objects.all().delete() + setattr(settings, settings.DISPATCHERD_FEATURE_FLAG_NAME, True) + setattr(settings, settings.ANALYTICS_FEATURE_FLAG_NAME, False) + seed_feature_flags() assert features.DISPATCHERD is True assert features.ANALYTICS is False @@ -45,18 +45,15 @@ def test_get_feature_flag(settings): @pytest.mark.django_db def test_feature_flag_caching(settings): """Test that feature flag values are properly cached.""" - settings.FLAGS = { - settings.DISPATCHERD_FEATURE_FLAG_NAME: [ - {"condition": "boolean", "value": True} - ] - } - + AAPFlag.objects.all().delete() + setattr(settings, settings.DISPATCHERD_FEATURE_FLAG_NAME, True) + seed_feature_flags() # First access - should cache the value assert features.DISPATCHERD is True # Change the underlying flag value - settings.FLAGS[settings.DISPATCHERD_FEATURE_FLAG_NAME][0]["value"] = False - + setattr(settings, settings.DISPATCHERD_FEATURE_FLAG_NAME, False) + seed_feature_flags() # Should still get the cached value assert features.DISPATCHERD is True @@ -64,21 +61,22 @@ def test_feature_flag_caching(settings): @pytest.mark.django_db def test_cache_invalidation(settings): """Test that cache invalidation works as expected.""" - settings.FLAGS = { - settings.DISPATCHERD_FEATURE_FLAG_NAME: [ - {"condition": "boolean", "value": True} - ] - } + AAPFlag.objects.all().delete() + setattr(settings, settings.DISPATCHERD_FEATURE_FLAG_NAME, True) + seed_feature_flags() # Populate cache assert features.DISPATCHERD is True # Change the flag value and clear cache - settings.FLAGS[settings.DISPATCHERD_FEATURE_FLAG_NAME][0]["value"] = False + setattr(settings, settings.DISPATCHERD_FEATURE_FLAG_NAME, False) + seed_feature_flags() _get_feature.cache_clear() - # Should get the new value after cache clear - assert features.DISPATCHERD is False + # Feature should remain true. + # If runtime toggle, we should only be able to + # update the value after toggling it via the platform gateway + assert features.DISPATCHERD is True @pytest.mark.django_db