Skip to content

Commit 8683e6c

Browse files
committed
Update DAB PoC
1 parent 5ec80eb commit 8683e6c

File tree

9 files changed

+130
-20
lines changed

9 files changed

+130
-20
lines changed

ansible_base/feature_flags/migrations/0001_initial.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.17 on 2025-03-26 17:52
1+
# Generated by Django 4.2.17 on 2025-04-08 14:36
22

33
import ansible_base.feature_flags.models.aap_flag
44
from django.conf import settings
@@ -22,13 +22,15 @@ class Migration(migrations.Migration):
2222
('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')),
2323
('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')),
2424
('name', models.CharField(help_text='The name of the feature flag. Must follow the format of FEATURE_<flag-name>_ENABLED.', max_length=64, validators=[ansible_base.feature_flags.models.aap_flag.validate_feature_flag_name])),
25+
('ui_name', models.CharField(help_text='The pretty name to display in the application User Interface', max_length=64)),
2526
('condition', models.CharField(default='boolean', help_text='Used to specify a condition, which if met, will enable the feature flag.', max_length=64)),
2627
('value', models.CharField(default='True', help_text='The value used to evaluate the conditional specified.', max_length=127)),
2728
('required', models.BooleanField(default=False, help_text="If multiple conditions are required to be met to enable a feature flag, 'required' can be used to specify the necessary conditionals.")),
2829
('support_level', models.CharField(choices=[('NOT_FOR_USE', 'Not for use'), ('NOT_FOR_PRODUCTION', 'Not for production'), ('READY_FOR_PRODUCTION', 'Ready for production')], help_text='The support criteria for the feature flag. Must be one of (NOT_FOR_USE, NOT_FOR_PRODUCTION, READY_FOR_PRODUCTION).', max_length=25)),
2930
('visibility', models.CharField(choices=[('public', 'public'), ('private', 'private')], help_text='The visibility level of the feature flag. If private, flag is hidden.', max_length=20)),
3031
('toggle_type', models.CharField(choices=[('install-time', 'install-time'), ('run-time', 'run-time')], default='run-time', help_text="Details whether a flag is toggle-able at run-time or install-time. (Default: 'run-time').", max_length=20)),
31-
('description', models.CharField(default='', help_text='A detailed description giving an overview of the feature flag.', max_length=300)),
32+
('description', models.CharField(default='', help_text='A detailed description giving an overview of the feature flag.', max_length=500)),
33+
('support_url', models.CharField(blank=True, default='', help_text='A link to the documentation support URL for the feature', max_length=250)),
3234
('labels', models.JSONField(blank=True, default=list, help_text='A list of labels for the feature flag.', null=True)),
3335
('created_by', models.ForeignKey(default=None, editable=False, help_text='The user who created this resource.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created+', to=settings.AUTH_USER_MODEL)),
3436
('modified_by', models.ForeignKey(default=None, editable=False, help_text='The user who last modified this resource.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_modified+', to=settings.AUTH_USER_MODEL)),

ansible_base/feature_flags/models/aap_flag.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __str__(self):
2525
required=" (required)" if self.required else "",
2626
)
2727

28-
resource = AnsibleResourceField(primary_key_field="id")
28+
# resource = AnsibleResourceField(primary_key_field="id")
2929

3030
name = models.CharField(
3131
max_length=64,
@@ -34,6 +34,12 @@ def __str__(self):
3434
validators=[validate_feature_flag_name],
3535
blank=False,
3636
)
37+
ui_name = models.CharField(
38+
max_length=64,
39+
null=False,
40+
blank=False,
41+
help_text=_("The pretty name to display in the application User Interface")
42+
)
3743
condition = models.CharField(max_length=64, default="boolean", help_text=_("Used to specify a condition, which if met, will enable the feature flag."))
3844
value = models.CharField(max_length=127, default="True", help_text=_("The value used to evaluate the conditional specified."))
3945
required = models.BooleanField(
@@ -61,5 +67,6 @@ def __str__(self):
6167
default='run-time',
6268
help_text=_("Details whether a flag is toggle-able at run-time or install-time. (Default: 'run-time')."),
6369
)
64-
description = models.CharField(max_length=300, null=False, default="", help_text=_("A detailed description giving an overview of the feature flag."))
70+
description = models.CharField(max_length=500, null=False, default="", help_text=_("A detailed description giving an overview of the feature flag."))
71+
support_url = models.CharField(max_length=250, null=False, default="", blank=True, help_text="A link to the documentation support URL for the feature")
6572
labels = models.JSONField(null=True, default=list, help_text=_("A list of labels for the feature flag."), blank=True)
Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
from flags.state import flag_state
22
from rest_framework import serializers
33

4+
from ansible_base.feature_flags.models import AAPFlag
5+
from ansible_base.lib.serializers.common import NamedCommonModelSerializer
6+
47
from .utils import get_django_flags
58

69

7-
# TODO: This view and its serializer can be removed after functionality is migrated over to new class
8-
class FeatureFlagSerializer(serializers.Serializer):
10+
class FeatureFlagSerializer(NamedCommonModelSerializer):
11+
"""Serialize list of feature flags"""
12+
13+
state = serializers.SerializerMethodField()
14+
15+
class Meta:
16+
model = AAPFlag
17+
fields = NamedCommonModelSerializer.Meta.fields + [x.name for x in AAPFlag._meta.concrete_fields] + ['state']
18+
read_only_fields = ["name", "condition", "required", "support_level", "visibility", "toggle_type", "description", "labels"]
19+
20+
def get_state(self, instance):
21+
return flag_state(instance.name)
22+
23+
def to_representation(self, instance):
24+
instance.state = True
25+
ret = super().to_representation(instance)
26+
return ret
27+
28+
29+
# TODO: Remove once all components are migrated to the new endpont.
30+
class OldFeatureFlagSerializer(NamedCommonModelSerializer):
931
"""Serialize list of feature flags"""
1032

33+
class Meta:
34+
model = AAPFlag
35+
fields = NamedCommonModelSerializer.Meta.fields + [x.name for x in AAPFlag._meta.concrete_fields]
36+
read_only_fields = ["name", "condition", "required", "support_level", "visibility", "toggle_type", "description", "labels"]
37+
1138
def to_representation(self) -> dict:
1239
return_data = {}
1340
feature_flags = get_django_flags()
1441
for feature_flag in feature_flags:
1542
return_data[feature_flag] = flag_state(feature_flag)
16-
1743
return return_data

ansible_base/feature_flags/urls.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
from django.urls import path
1+
from django.urls import include, path
22

33
from ansible_base.feature_flags import views
44
from ansible_base.feature_flags.apps import FeatureFlagsConfig
5+
from ansible_base.lib.routers import AssociationResourceRouter
56

67
app_name = FeatureFlagsConfig.label
78

8-
api_version_urls = [
9-
path('feature_flags_state/', views.FeatureFlagsStateListView.as_view(), name='featureflags-list'),
10-
]
9+
router = AssociationResourceRouter()
10+
11+
router.register(r'feature_flags', views.FeatureFlagsView, basename='aap_flags')
12+
13+
# TODO: Remove once all components are migrated to new endpoints.
14+
api_version_urls = [path('feature_flags_state/', views.OldFeatureFlagsStateListView.as_view(), name='featureflags-list'), path('', include(router.urls))]
15+
1116
api_urls = []
1217
root_urls = []

ansible_base/feature_flags/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def update_feature_flag(existing: AAPFlag, new):
2828

2929
existing.support_level = new['support_level']
3030
existing.visibility = new['visibility']
31+
existing.ui_name = new['ui_name']
32+
existing.support_url = new['support_url']
3133
if 'required' in new:
3234
existing.required = new['required']
3335
if 'toggle_type' in new:

ansible_base/feature_flags/views.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
1+
from django.shortcuts import get_object_or_404
12
from django.utils.translation import gettext_lazy as _
3+
from flags.state import flag_enabled, flag_state, get_flags
4+
from rest_framework import status
25
from rest_framework.response import Response
6+
from rest_framework.viewsets import ModelViewSet
37

4-
from ansible_base.feature_flags.serializers import FeatureFlagSerializer
8+
from ansible_base.feature_flags.models import AAPFlag
9+
from ansible_base.feature_flags.serializers import FeatureFlagSerializer, OldFeatureFlagSerializer
510
from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView
11+
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
12+
from ansible_base.lib.utils.views.permissions import IsSuperuserOrAuditor
13+
from ansible_base.rest_pagination import DefaultPaginator
614

7-
from .utils import get_django_flags
15+
from .utils import get_django_flags, is_boolean_str
816

917

10-
# TODO: This view and its serializer can be removed after functionality is migrated over to new class
11-
class FeatureFlagsStateListView(AnsibleBaseView):
18+
class FeatureFlagsView(AnsibleBaseDjangoAppApiView, ModelViewSet):
1219
"""
1320
A view class for displaying feature flags
1421
"""
1522

23+
queryset = AAPFlag.objects.order_by('id')
1624
serializer_class = FeatureFlagSerializer
25+
permission_classes = [IsSuperuserOrAuditor]
26+
http_method_names = ['get', 'put', 'head', 'options']
27+
28+
def update(self, request, **kwargs):
29+
_feature_flag = self.get_object()
30+
value = request.data.get('value')
31+
if not value:
32+
return Response(status=status.HTTP_400_BAD_REQUEST, data={"details": "Invalid request object."})
33+
34+
# Disable runtime toggle if the feature flag feature is not enabled
35+
if not flag_enabled('FEATURE_FEATURE_FLAGS_ENABLED'):
36+
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data={"details": "Runtime feature flags toggle is not enabled."})
37+
38+
feature_flag = get_object_or_404(AAPFlag, pk=_feature_flag.id)
39+
if feature_flag.toggle_type == 'install-time':
40+
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data={"details": "Install-time feature flags cannot be toggled at run-time."})
41+
if feature_flag.condition == "boolean" and not is_boolean_str(value):
42+
return Response(status=status.HTTP_400_BAD_REQUEST, data={"details": "Feature flag boolean conditional requires using boolean value."})
43+
feature_flag.value = value
44+
feature_flag.save()
45+
46+
return Response(self.get_serializer().to_representation(feature_flag))
47+
48+
49+
# TODO: This can be removed after functionality is migrated over to new class
50+
class OldFeatureFlagsStateListView(AnsibleBaseView):
51+
"""
52+
A view class for displaying feature flags
53+
"""
54+
55+
serializer_class = OldFeatureFlagSerializer
1756
filter_backends = []
1857
name = _('Feature Flags')
1958
http_method_names = ['get', 'head']
2059

2160
def get(self, request, format=None):
22-
self.serializer = FeatureFlagSerializer()
61+
self.serializer = OldFeatureFlagSerializer()
2362
return Response(self.serializer.to_representation())
2463

2564
def get_queryset(self):

ansible_base/lib/dynamic_config/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
load_envvars,
66
load_python_file_with_injected_context,
77
load_standard_settings_files,
8+
toggle_database_feature_flags,
89
toggle_feature_flags,
910
validate,
1011
)
@@ -17,5 +18,6 @@
1718
"load_python_file_with_injected_context",
1819
"load_standard_settings_files",
1920
"toggle_feature_flags",
21+
"toggle_database_feature_flags",
2022
"validate",
2123
]

ansible_base/lib/dynamic_config/dynaconf_helpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from dynaconf.loaders.yaml_loader import yaml
1616
from dynaconf.utils.files import glob
1717
from dynaconf.utils.functional import empty
18+
from flags.sources import get_flags
19+
from flags.state import disable_flag, enable_flag
1820

1921
from ansible_base.lib.dynamic_config.settings_logic import get_mergeable_dab_settings
2022

@@ -315,3 +317,17 @@ def toggle_feature_flags(settings: Dynaconf) -> dict[str, Any]:
315317
feature_content[0]["value"] = installer_value
316318
data[f"FLAGS__{feature_name}"] = feature_content
317319
return data
320+
321+
322+
def toggle_database_feature_flags(settings: Dynaconf) -> dict[str, Any]:
323+
"""Toggle FLAGS based on installer settings.
324+
FLAGS is a django-flags formatted dictionary.
325+
Installers will place `FEATURE_SOME_PLATFORM_FLAG_ENABLED=True/False` in the settings file.
326+
This function will update the value in the database with the expected boolean value
327+
"""
328+
for feature_name in get_flags():
329+
if (installer_value := settings.get(feature_name, empty)) is not empty:
330+
if installer_value:
331+
enable_flag(feature_name)
332+
else:
333+
disable_flag(feature_name)

ansible_base/lib/dynamic_config/feature_flags/platform_flags.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,63 @@
33

44
class AAPFlagNameSchema(TypedDict):
55
name: str
6+
ui_name: str
67
condition: str
78
value: str
8-
visibility: str
9+
required: str
910
support_level: str
11+
visibility: str
12+
toggle_type: str
1013
description: str
14+
support_url: str
1115
labels: list
12-
toggle_type: str
1316

1417

1518
AAP_FEATURE_FLAGS: list[AAPFlagNameSchema] = [
1619
AAPFlagNameSchema(
1720
name="FEATURE_FEATURE_FLAGS_ENABLED",
21+
ui_name="Feature Flags",
1822
condition="boolean",
1923
value="True",
20-
visibility="public",
2124
support_level='READY_FOR_PRODUCTION',
25+
visibility="public",
26+
toggle_type='install-time',
2227
description='If enabled, feature flags can be toggled on/off at runtime via UI or API. '
2328
'If disabled, feature flags can only be toggled on/off at install-time.',
29+
support_url="",
2430
labels=['platform'],
25-
toggle_type='install-time',
2631
),
2732
AAPFlagNameSchema(
2833
name="FEATURE_INDIRECT_NODE_COUNTING_ENABLED",
34+
ui_name="Indirect Node Counting",
2935
visibility="public",
3036
condition="boolean",
3137
value="False",
3238
support_level="NOT_FOR_PRODUCTION",
3339
description="TBD",
40+
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
3441
labels=['controller'],
3542
),
3643
AAPFlagNameSchema(
3744
name="FEATURE_POLICY_AS_CODE_ENABLED",
45+
ui_name="Policy as Code",
3846
visibility="public",
3947
condition="boolean",
4048
value="False",
4149
support_level="NOT_FOR_PRODUCTION",
4250
description="TBD",
51+
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
4352
labels=['controller'],
4453
),
4554
AAPFlagNameSchema(
4655
name="FEATURE_EDA_ANALYTICS_ENABLED",
56+
ui_name="Event-Driven Ansible Analytics",
4757
condition="boolean",
4858
value="False",
4959
visibility="public",
5060
support_level="NOT_FOR_PRODUCTION",
5161
description="TBD",
62+
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
5263
labels=['eda'],
5364
),
5465
]

0 commit comments

Comments
 (0)