diff --git a/poetry.lock b/poetry.lock index 30204221e..9d2a3c6cc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -660,14 +660,14 @@ files = [ [[package]] name = "django" -version = "4.2.16" +version = "4.2.22" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, - {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, + {file = "django-4.2.22-py3-none-any.whl", hash = "sha256:0a32773b5b7f4e774a155ee253ab24a841fed7e9e9061db08bf2ce9711da404d"}, + {file = "django-4.2.22.tar.gz", hash = "sha256:e726764b094407c313adba5e2e866ab88f00436cad85c540a5bf76dc0a912c9e"}, ] [package.dependencies] @@ -681,7 +681,7 @@ bcrypt = ["bcrypt"] [[package]] name = "django-ansible-base" -version = "2025.5.8" +version = "25.6.17.0.dev663+gd8be258" description = "A Django app used by ansible services" optional = false python-versions = ">=3.9" @@ -693,7 +693,7 @@ develop = false asgiref = {version = "*", optional = true, markers = "extra == \"resource-registry\""} channels = {version = "*", optional = true, markers = "extra == \"channel-auth\""} cryptography = "*" -Django = ">=4.2.16,<4.3.0" +Django = ">=4.2.21,<4.3.0" django-crum = "*" django-flags = {version = "*", optional = true, markers = "extra == \"feature-flags\""} django-redis = {version = "*", optional = true, markers = "extra == \"redis-client\""} @@ -707,9 +707,9 @@ sqlparse = ">=0.5.2" urllib3 = {version = "*", optional = true, markers = "extra == \"resource-registry\""} [package.extras] -all = ["asgiref", "channels", "cryptography", "django-auth-ldap", "django-flags", "django-oauth-toolkit (<2.4.0)", "django-redis", "drf-spectacular", "pyjwt", "pyjwt", "pyrad", "pytest", "pytest-django", "python-ldap", "python3-saml", "redis", "requests", "requests", "social-auth-app-django (==5.4.1)", "social-auth-core (<=4.5.4)", "tabulate", "tacacs_plus", "urllib3", "xmlsec (==1.3.13)"] +all = ["asgiref", "channels", "cryptography", "django-auth-ldap", "django-flags", "django-oauth-toolkit (<2.4.0)", "django-redis", "drf-spectacular", "ldap-filter", "pyjwt", "pyjwt", "pyrad", "pytest", "pytest-django", "python-ldap", "python3-saml", "redis", "requests", "requests", "social-auth-app-django (==5.4.1)", "social-auth-core (<=4.5.4)", "tabulate", "tacacs_plus", "urllib3", "xmlsec (==1.3.13)"] api-documentation = ["drf-spectacular"] -authentication = ["django-auth-ldap", "pyrad", "python-ldap", "python3-saml", "social-auth-app-django (==5.4.1)", "social-auth-core (<=4.5.4)", "tabulate", "tacacs_plus", "xmlsec (==1.3.13)"] +authentication = ["django-auth-ldap", "ldap-filter", "pyrad", "python-ldap", "python3-saml", "social-auth-app-django (==5.4.1)", "social-auth-core (<=4.5.4)", "tabulate", "tacacs_plus", "xmlsec (==1.3.13)"] channel-auth = ["channels"] feature-flags = ["django-flags"] jwt-consumer = ["pyjwt", "requests"] @@ -720,9 +720,9 @@ testing = ["cryptography", "pytest", "pytest-django"] [package.source] type = "git" -url = "https://github.com/ansible/django-ansible-base.git" -reference = "2025.5.8" -resolved_reference = "a46ebe5efa8eea15d5943301d866f204e82f6af4" +url = "https://github.com/zkayyali812/django-ansible-base.git" +reference = "phase2/feature-flags/poc" +resolved_reference = "d8be25892d8239210d18a0be0df492cfa01325d6" [[package]] name = "django-crum" @@ -3024,4 +3024,4 @@ dev = ["psycopg-binary"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "407ebe011c6024e245c6f1a50fda29dcdcca9b8429e3e007e74eaac7d5135b68" +content-hash = "70130491087003a7131da493297e07bfdf1bb7cfa759cd45719fba6fde4ab6ff" diff --git a/pyproject.toml b/pyproject.toml index ce36e4b8b..8ca417c49 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.5.8", extras = [ +django-ansible-base = { git = "https://github.com/zkayyali812/django-ansible-base.git", branch = "phase2/feature-flags/poc", extras = [ "channel-auth", "rbac", "redis-client", diff --git a/src/aap_eda/api/resource_api.py b/src/aap_eda/api/resource_api.py index 52d17f2b1..f386d6dfb 100644 --- a/src/aap_eda/api/resource_api.py +++ b/src/aap_eda/api/resource_api.py @@ -11,7 +11,7 @@ # 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. - +from ansible_base.feature_flags.models import AAPFlag from ansible_base.resource_registry.registry import ( ParentResource, ResourceConfig, @@ -19,6 +19,7 @@ SharedResource, ) from ansible_base.resource_registry.shared_types import ( + FeatureFlagType, OrganizationType, TeamType, UserType, @@ -52,4 +53,10 @@ class APIConfig(ServiceAPIConfig): serializer=OrganizationType, 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 42ccc5152..4908baa2e 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 8cc3681a3..6c48f888e 100644 --- a/tests/integration/api/test_root.py +++ b/tests/integration/api/test_root.py @@ -83,6 +83,9 @@ "/organizations/", "/teams/", "/event-streams/", + "feature_flags/states/", + # To be removed after all components + # have migrated away from this endpoint "/feature_flags_state/", ], False, 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