Skip to content

Commit b8f9449

Browse files
committed
resource-sync
1 parent 98b5c92 commit b8f9449

File tree

17 files changed

+113
-87
lines changed

17 files changed

+113
-87
lines changed

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
**/.github
2+
**/.cache
3+
**/.gitignore
4+
**/.venv
5+
**/venv
6+
**/.tox

ansible_base/feature_flags/apps.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
from django.apps import AppConfig
2-
from django.db.models.signals import post_migrate
3-
from django.db.utils import OperationalError, ProgrammingError
4-
5-
from .utils import create_initial_data
62

73

84
class FeatureFlagsConfig(AppConfig):
95
default_auto_field = 'django.db.models.BigAutoField'
106
name = 'ansible_base.feature_flags'
117
label = 'dab_feature_flags'
128
verbose_name = 'Feature Flags'
13-
14-
def ready(self):
15-
from django.conf import settings
16-
17-
if 'ansible_base.feature_flags' in settings.INSTALLED_APPS:
18-
# TODO: Is there a better way to handle this logic?
19-
20-
# If migrations are complete, attempt to load in feature flags again.
21-
# This can help capture any updates to the platform flags loaded in to ensure that new values
22-
# are added and updated.
23-
# Otherwise wait for migrations to be complete before loading in feature flags.
24-
try:
25-
create_initial_data()
26-
except (ProgrammingError, OperationalError):
27-
post_migrate.connect(create_initial_data)

ansible_base/feature_flags/migrations/0001_initial.py

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

33
import ansible_base.feature_flags.models.aap_flag
44
from django.conf import settings
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
3636
('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)),
3737
],
3838
options={
39-
'unique_together': {('name', 'condition', 'value')},
39+
'unique_together': {('name', 'condition')},
4040
},
4141
),
4242
]

ansible_base/feature_flags/models/aap_flag.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,19 @@
22
from django.db import models
33
from django.utils.translation import gettext_lazy as _
44

5-
from ansible_base.activitystream.models import AuditableModel
65
from ansible_base.lib.abstract_models.common import NamedCommonModel
7-
8-
# from ansible_base.resource_registry.fields import AnsibleResourceField
6+
from ansible_base.resource_registry.fields import AnsibleResourceField
97

108

119
def validate_feature_flag_name(value: str):
1210
if not value.startswith('FEATURE_') or not value.endswith('_ENABLED'):
1311
raise ValidationError(_("Feature flag names must follow the format of `FEATURE_<flag-name>_ENABLED`"))
1412

1513

16-
class AAPFlag(NamedCommonModel, AuditableModel):
14+
class AAPFlag(NamedCommonModel):
1715
class Meta:
1816
app_label = "dab_feature_flags"
19-
unique_together = ("name", "condition", "value")
17+
unique_together = ("name", "condition")
2018

2119
def __str__(self):
2220
return "{name} is enabled when {condition} is " "{value}{required}".format(
@@ -26,7 +24,7 @@ def __str__(self):
2624
required=" (required)" if self.required else "",
2725
)
2826

29-
# resource = AnsibleResourceField(primary_key_field="id")
27+
resource = AnsibleResourceField(primary_key_field="id")
3028

3129
name = models.CharField(
3230
max_length=64,

ansible_base/feature_flags/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class FeatureFlagSerializer(NamedCommonModelSerializer):
1515
class Meta:
1616
model = AAPFlag
1717
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"]
18+
read_only_fields = ["name", "condition", "required", "support_level", "visibility", "toggle_type", "description", "labels", "ui_name", "support_url"]
1919

2020
def get_state(self, instance):
2121
return flag_state(instance.name)
@@ -35,7 +35,7 @@ class Meta:
3535
fields = NamedCommonModelSerializer.Meta.fields + [x.name for x in AAPFlag._meta.concrete_fields]
3636
read_only_fields = ["name", "condition", "required", "support_level", "visibility", "toggle_type", "description", "labels"]
3737

38-
def to_representation(self) -> dict:
38+
def to_representation(self, instance=None) -> dict:
3939
return_data = {}
4040
feature_flags = get_django_flags()
4141
for feature_flag in feature_flags:

ansible_base/feature_flags/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
router = AssociationResourceRouter()
1010

11+
router.register(r'feature_flags/states', views.FeatureFlagsStatesView, basename='aap_flags_states')
1112
router.register(r'feature_flags', views.FeatureFlagsView, basename='aap_flags')
1213

1314
# TODO: Remove once all components are migrated to new endpoints.

ansible_base/feature_flags/utils.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1+
import logging
2+
13
from django.apps import apps
24
from django.conf import settings
35
from django.core.exceptions import ValidationError
46
from flags.sources import get_flags
57

68
from ansible_base.lib.dynamic_config.feature_flags.platform_flags import AAP_FEATURE_FLAGS
79

10+
logger = logging.getLogger('ansible_base.feature_flags.utils')
11+
812

913
def get_django_flags():
1014
return get_flags()
1115

1216

13-
def is_boolean_str(val):
14-
return val.lower() in {'true', 'false'}
15-
16-
1717
def create_initial_data(**kwargs):
1818
"""
1919
Loads in platform feature flags when the server starts
@@ -26,20 +26,15 @@ def update_feature_flag(existing: AAPFlag, new):
2626
Update only the required fields of the feature flag model.
2727
This is used to ensure that flags can be loaded in when the server starts, with any applicable updates.
2828
"""
29-
30-
existing.support_level = new['support_level']
31-
existing.visibility = new['visibility']
32-
existing.ui_name = new['ui_name']
33-
existing.support_url = new['support_url']
34-
if 'required' in new:
35-
existing.required = new['required']
36-
if 'toggle_type' in new:
37-
existing.toggle_type = new['toggle_type']
38-
if 'labels' in new:
39-
existing.labels = new['labels']
40-
if 'description' in new:
41-
existing.description = new['description']
42-
existing.save()
29+
existing.support_level = new.get('support_level')
30+
existing.visibility = new.get('visibility')
31+
existing.ui_name = new.get('ui_name')
32+
existing.support_url = new.get('support_url')
33+
existing.required = new.get('required', False)
34+
existing.toggle_type = new.get('toggle_type', 'run-time')
35+
existing.labels = new.get('labels', [])
36+
existing.description = new.get('description', '')
37+
return existing
4338

4439
def load_feature_flags():
4540
"""
@@ -50,19 +45,20 @@ def load_feature_flags():
5045
try:
5146
existing_flag = FeatureFlags.objects.filter(name=flag['name'], condition=flag['condition'])
5247
if existing_flag:
53-
update_feature_flag(existing_flag.first(), flag)
48+
feature_flag = update_feature_flag(existing_flag.first(), flag)
5449
else:
5550
if hasattr(settings, flag['name']):
5651
flag['value'] = getattr(settings, flag['name'])
57-
FeatureFlags.objects.create(**flag)
58-
AAPFlag(**flag).full_clean()
52+
feature_flag = FeatureFlags(**flag)
53+
feature_flag.full_clean()
54+
feature_flag.save()
5955
except ValidationError as e:
6056
# Ignore this error unless better way to bypass this
61-
if e.messages[0] == 'Aap flag with this Name, Condition and Value already exists.':
57+
if e.messages[0] == 'Aap flag with this Name and Condition already exists.':
6258
pass
6359
else:
64-
# Raise row validation errors
65-
raise e
60+
error_msg = f"Invalid feature flag: {flag['name']}. Error: {e}"
61+
logger.error(error_msg)
6662

6763
def delete_feature_flags():
6864
"""

ansible_base/feature_flags/views.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
from django.shortcuts import get_object_or_404
21
from django.utils.translation import gettext_lazy as _
3-
from flags.state import flag_enabled
4-
from rest_framework import status
2+
from flags.sources import get_flags
3+
from flags.state import flag_state
54
from rest_framework.response import Response
65
from rest_framework.viewsets import ModelViewSet
76

87
from ansible_base.feature_flags.models import AAPFlag
98
from ansible_base.feature_flags.serializers import FeatureFlagSerializer, OldFeatureFlagSerializer
109
from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView
1110
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
12-
from ansible_base.lib.utils.views.permissions import IsSuperuserOrAuditor
11+
from ansible_base.lib.utils.views.permissions import IsSuperuserOrAuditor, try_add_oauth2_scope_permission
1312

14-
from .utils import get_django_flags, is_boolean_str
13+
# from ansible_base.oauth2_provider.permissions import OAuth2ScopePermission
14+
from ansible_base.rest_pagination import DefaultPaginator
15+
16+
from .utils import get_django_flags
1517

1618

1719
class FeatureFlagsView(AnsibleBaseDjangoAppApiView, ModelViewSet):
@@ -21,24 +23,27 @@ class FeatureFlagsView(AnsibleBaseDjangoAppApiView, ModelViewSet):
2123

2224
queryset = AAPFlag.objects.order_by('id')
2325
serializer_class = FeatureFlagSerializer
24-
permission_classes = [IsSuperuserOrAuditor]
25-
http_method_names = ['get', 'put', 'head', 'options']
26-
27-
def update(self, request, **kwargs):
28-
_feature_flag = self.get_object()
29-
value = request.data.get('value')
30-
if not value:
31-
return Response(status=status.HTTP_400_BAD_REQUEST, data={"details": "Invalid request object."})
32-
33-
feature_flag = get_object_or_404(AAPFlag, pk=_feature_flag.id)
34-
if feature_flag.toggle_type == 'install-time':
35-
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED, data={"details": "Install-time feature flags cannot be toggled at run-time."})
36-
if feature_flag.condition == "boolean" and not is_boolean_str(value):
37-
return Response(status=status.HTTP_400_BAD_REQUEST, data={"details": "Feature flag boolean conditional requires using boolean value."})
38-
feature_flag.value = value
39-
feature_flag.save()
40-
41-
return Response(self.get_serializer().to_representation(feature_flag))
26+
permission_classes = try_add_oauth2_scope_permission([IsSuperuserOrAuditor])
27+
http_method_names = ['get', 'head', 'options']
28+
29+
30+
class FeatureFlagsStatesView(AnsibleBaseDjangoAppApiView, ModelViewSet):
31+
"""
32+
A view class for displaying feature flags states
33+
"""
34+
35+
queryset = AAPFlag.objects.order_by('id')
36+
permission_classes = try_add_oauth2_scope_permission([IsSuperuserOrAuditor])
37+
http_method_names = ['get', 'head', 'options']
38+
39+
def list(self, request):
40+
paginator = DefaultPaginator()
41+
flags = get_flags()
42+
ret = []
43+
for flag in flags:
44+
ret.append({"flag_name": flag, "flag_state": flag_state(flag)})
45+
result_page = paginator.paginate_queryset(ret, request)
46+
return paginator.get_paginated_response(result_page)
4247

4348

4449
# TODO: This can be removed after functionality is migrated over to new class

ansible_base/lib/dynamic_config/feature_flags/platform_flags.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,26 @@ class AAPFlagNameSchema(TypedDict):
4949
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
5050
labels=['eda'],
5151
),
52+
AAPFlagNameSchema(
53+
name="FEATURE_GATEWAY_IPV6_USAGE_ENABLED",
54+
ui_name="Gateway IPv6 Usage",
55+
condition="boolean",
56+
value="False",
57+
visibility="private",
58+
support_level="NOT_FOR_PRODUCTION",
59+
description="TBD",
60+
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
61+
labels=['gateway'],
62+
),
63+
AAPFlagNameSchema(
64+
name="FEATURE_GATEWAY_CREATE_CRC_SERVICE_TYPE",
65+
ui_name="Gateway Create CRC Service Type",
66+
condition="boolean",
67+
value="False",
68+
visibility="private",
69+
support_level="NOT_FOR_PRODUCTION",
70+
description="TBD",
71+
support_url="https://docs.redhat.com/en/documentation/red_hat_ansible_automation_platform/2.5/",
72+
labels=['gateway'],
73+
),
5274
]

ansible_base/rbac/permission_registry.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def create_managed_roles(self, apps) -> list[tuple[Model, bool]]:
143143
return ret
144144

145145
def call_when_apps_ready(self, apps, app_config) -> None:
146+
from ansible_base.feature_flags.utils import create_initial_data as feature_flag_create_initial_data
146147
from ansible_base.rbac import triggers
147148
from ansible_base.rbac.evaluations import bound_has_obj_perm, bound_singleton_permissions, connect_rbac_methods
148149
from ansible_base.rbac.management import create_dab_permissions
@@ -172,6 +173,11 @@ def call_when_apps_ready(self, apps, app_config) -> None:
172173
sender=app_config,
173174
dispatch_uid="ansible_base.rbac.triggers.post_migration_rbac_setup",
174175
)
176+
if 'ansible_base.feature_flags' in settings.INSTALLED_APPS:
177+
try:
178+
feature_flag_create_initial_data()
179+
except Exception:
180+
post_migrate.connect(feature_flag_create_initial_data, sender=self)
175181

176182
self.user_model.add_to_class('has_obj_perm', bound_has_obj_perm)
177183
self.user_model.add_to_class('singleton_permissions', bound_singleton_permissions)

0 commit comments

Comments
 (0)