Skip to content

Commit 26c318a

Browse files
Add Feature Flag List state API (ansible#685)
# [[AAP-39135](https://issues.redhat.com/browse/AAP-39135)] Add Feature Flag List API ## Description - What is being changed? Adds a get/list API for feature flags to the DAB for consumption by other components - Why is this change needed? To provide a common API for other components to get/modify the FLAGS variable - How does this change address the issue? Adds a list/get feature flags API ## Type of Change <!-- Mandatory: Check one or more boxes that apply --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Documentation update - [x] Test update - [ ] Refactoring (no functional changes) - [ ] Development environment change - [ ] Configuration change ## Self-Review Checklist <!-- These items help ensure quality - they complement our automated CI checks --> - [x] I have performed a self-review of my code - [x] I have added relevant comments to complex code sections - [x] I have updated documentation where needed - [x] I have considered the security impact of these changes - [x] I have considered performance implications - [x] I have thought about error handling and edge cases - [x] I have tested the changes in my local environment ## Testing Instructions <!-- Optional for test-only changes. Mandatory for all other changes --> Deploy local environment and visit - 1. http://127.0.0.1:8000/api/v1/feature_flags_state/ 2. Also run unit tests ### Expected Results Feature flags get/list API should work. In test-app, we should see 3 dummy feature flags. --------- Co-authored-by: Joe Shimkus <[email protected]>
1 parent 5a45120 commit 26c318a

File tree

16 files changed

+268
-0
lines changed

16 files changed

+268
-0
lines changed

ansible_base/feature_flags/__init__.py

Whitespace-only changes.

ansible_base/feature_flags/apps.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.apps import AppConfig
2+
3+
4+
class FeatureFlagsConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'ansible_base.feature_flags'
7+
label = 'dab_feature_flags'
8+
verbose_name = 'Feature Flags'

ansible_base/feature_flags/migrations/__init__.py

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from flags.state import flag_state
2+
from rest_framework import serializers
3+
4+
from .utils import get_django_flags
5+
6+
7+
class FeatureFlagSerializer(serializers.Serializer):
8+
"""Serialize list of feature flags"""
9+
10+
def to_representation(self) -> dict:
11+
return_data = {}
12+
feature_flags = get_django_flags()
13+
for feature_flag in feature_flags:
14+
return_data[feature_flag] = flag_state(feature_flag)
15+
16+
return return_data

ansible_base/feature_flags/urls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.urls import path
2+
3+
from ansible_base.feature_flags import views
4+
from ansible_base.feature_flags.apps import FeatureFlagsConfig
5+
6+
app_name = FeatureFlagsConfig.label
7+
8+
api_version_urls = [
9+
path('feature_flags_state/', views.FeatureFlagsStateListView.as_view(), name='featureflags-list'),
10+
]
11+
api_urls = []
12+
root_urls = []
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.conf import settings
2+
3+
4+
def get_django_flags():
5+
return getattr(settings, 'FLAGS', {})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.utils.translation import gettext_lazy as _
2+
from rest_framework.response import Response
3+
4+
from ansible_base.feature_flags.serializers import FeatureFlagSerializer
5+
from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView
6+
7+
from .utils import get_django_flags
8+
9+
10+
class FeatureFlagsStateListView(AnsibleBaseView):
11+
"""
12+
A view class for displaying feature flags
13+
"""
14+
15+
serializer_class = FeatureFlagSerializer
16+
filter_backends = []
17+
name = _('Feature Flags')
18+
http_method_names = ['get', 'head']
19+
20+
def get(self, request, format=None):
21+
self.serializer = FeatureFlagSerializer()
22+
return Response(self.serializer.to_representation())
23+
24+
def get_queryset(self):
25+
return get_django_flags()

ansible_base/lib/dynamic_config/dynamic_settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
except NameError:
5858
OAUTH2_PROVIDER = {}
5959

60+
try:
61+
TEMPLATES # noqa: F821
62+
except NameError:
63+
TEMPLATES = []
64+
6065
for key, value in get_dab_settings(
6166
installed_apps=INSTALLED_APPS,
6267
rest_framework=REST_FRAMEWORK,
@@ -65,6 +70,7 @@
6570
middleware=MIDDLEWARE,
6671
oauth2_provider=OAUTH2_PROVIDER,
6772
caches=CACHES if 'CACHES' in locals() else None, # noqa: F821
73+
templates=TEMPLATES,
6874
).items():
6975
if key in ANSIBLE_BASE_OVERRIDABLE_SETTINGS:
7076
ANSIBLE_BASE_OVERRIDDEN_SETTINGS.append(key)

ansible_base/lib/dynamic_config/settings_logic.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def get_dab_settings(
1818
middleware: Optional[list[str]] = None,
1919
oauth2_provider: Optional[dict] = None,
2020
caches: Optional[dict] = None,
21+
templates: Optional[list[dict]] = None,
2122
) -> dict:
2223
dab_data = {}
2324

@@ -297,4 +298,36 @@ def get_dab_settings(
297298
if PRIMARY_CACHE not in caches or FALLBACK_CACHE not in caches:
298299
raise RuntimeError(f'Cache definitions with the keys {PRIMARY_CACHE} and {FALLBACK_CACHE} must be defined when DABCacheWithFallback is used.')
299300

301+
if 'ansible_base.feature_flags' in installed_apps:
302+
dab_data.setdefault('INSTALLED_APPS', copy(installed_apps))
303+
if "flags" not in dab_data["INSTALLED_APPS"]:
304+
dab_data['INSTALLED_APPS'].append('flags')
305+
306+
dab_data.setdefault('TEMPLATES', copy(templates))
307+
found_template_backend = False
308+
template_context_processor = 'django.template.context_processors.request'
309+
# Look through all of the tmplates
310+
for template in dab_data['TEMPLATES']:
311+
# If this template has the BACKEND we care about...
312+
if template['BACKEND'] == 'django.template.backends.django.DjangoTemplates':
313+
found_template_backend = True
314+
# Look through all of its context processors
315+
found_context_processor = False
316+
for context_processor in template['OPTIONS']['context_processors']:
317+
if context_processor == template_context_processor:
318+
found_context_processor = True
319+
break
320+
# If we didn't find the context processor we care about append it
321+
if not found_context_processor:
322+
template['OPTIONS']['context_processors'].append(template_context_processor)
323+
324+
# If we never even found the backend, add one
325+
if not found_template_backend:
326+
dab_data['TEMPLATES'].append(
327+
{
328+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
329+
'OPTIONS': {'context_processors': [template_context_processor]},
330+
}
331+
)
332+
300333
return dab_data

docs/apps/feature_flags.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Feature Flags documentation
2+
3+
django-ansible-base uses django-flags to manage feature flags in the API.
4+
Additional library documentation can be found at https://cfpb.github.io/django-flags/
5+
6+
## Settings
7+
8+
Add `ansible_base.feature_flags` to your installed apps:
9+
10+
```python
11+
INSTALLED_APPS = [
12+
...
13+
'ansible_base.feature_flags',
14+
]
15+
```
16+
17+
### Additional Settings
18+
19+
Additional settings are required to enable feature_flags.
20+
This will happen automatically if using [dynamic_settings](../Installation.md)
21+
22+
First, you need to add `flags` to your `INSTALLED_APPS`:
23+
24+
```python
25+
INSTALLED_APPS = [
26+
...
27+
'flags',
28+
...
29+
]
30+
```
31+
32+
Additionally, create a `FLAGS` entry:
33+
34+
```python
35+
FLAGS = {}
36+
```
37+
38+
Finally, add `django.template.context_processors.request` to your `TEMPLATES` `context_processors` setting:
39+
40+
```python
41+
TEMPLATES = [
42+
{
43+
'BEACKEND': 'django.template.backends.django.DjangoTemplates',
44+
...
45+
'OPTIONS': {
46+
...
47+
'context_processors': [
48+
...
49+
'django.template.context_processors.request',
50+
...
51+
]
52+
...
53+
}
54+
...
55+
}
56+
]
57+
```
58+
59+
## URLS
60+
61+
This feature includes URLs which you will get if you are using [dynamic urls](../..//Installation.md)
62+
63+
If you want to manually add the urls without dynamic urls add the following to your urls.py:
64+
65+
```python
66+
from ansible_base.feature_flags import urls
67+
urlpatterns = [
68+
...
69+
path('api/v1/', include(feature_flags.api_version_urls)),
70+
...
71+
]
72+
```

0 commit comments

Comments
 (0)