Skip to content

Commit 9db7237

Browse files
fao89zkayyali812
andauthored
AAP-45875 Runtime Feature Flags (ansible#875)
This change is the foundation for enabling runtime platform feature flags for AAP. This updates the django-ansible-base to be the central location where all platform flags are defined. Components can inherit the `ansible_base.feature_flags` application to inherit all platform feature flag definitions. --------- Co-authored-by: Zack Kayyali <[email protected]>
1 parent 1a252cb commit 9db7237

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1518
-166
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/authentication/utils/claims.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def _add_rbac_role_mapping(has_permission, role_mapping, role, organization=None
217217

218218

219219
def _is_case_insensitivity_enabled() -> bool:
220-
return flag_enabled("FEATURE_CASE_INSENSITIVE_AUTH_MAPS")
220+
return flag_enabled("FEATURE_CASE_INSENSITIVE_AUTH_MAPS_ENABLED")
221221

222222

223223
def _lowercase_group_triggers(trigger_condition: dict) -> dict:

ansible_base/feature_flags/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from django.apps import AppConfig
2+
from django.db.models.signals import post_migrate
3+
4+
from ansible_base.feature_flags.utils import create_initial_data
25

36

47
class FeatureFlagsConfig(AppConfig):
58
default_auto_field = 'django.db.models.BigAutoField'
69
name = 'ansible_base.feature_flags'
710
label = 'dab_feature_flags'
811
verbose_name = 'Feature Flags'
12+
13+
def ready(self):
14+
post_migrate.connect(create_initial_data, sender=self)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
- name: FEATURE_INDIRECT_NODE_COUNTING_ENABLED
2+
ui_name: Indirect Node Counting
3+
visibility: True
4+
condition: boolean
5+
value: 'False'
6+
support_level: TECHNOLOGY_PREVIEW
7+
description: "Indirect Node Counting parses the event stream of all jobs to identify resources and stores these in the platform database. Example: Job automates VMware, the parser will report back the VMs, Hypervisors that were automated. This feature helps customers and partners report on the automations they are doing beyond an API endpoint."
8+
support_url: https://access.redhat.com/articles/7109910
9+
labels:
10+
- controller
11+
- name: FEATURE_EDA_ANALYTICS_ENABLED
12+
ui_name: Event-Driven Ansible Analytics
13+
visibility: False
14+
condition: boolean
15+
value: 'False'
16+
support_level: TECHNOLOGY_PREVIEW
17+
description: Submit Event-Driven Ansible usage analytics to console.redhat.com.
18+
support_url: https://access.redhat.com/solutions/7112810
19+
toggle_type: install-time
20+
labels:
21+
- eda
22+
- name: FEATURE_GATEWAY_IPV6_USAGE_ENABLED
23+
ui_name: Gateway IPv6 Enablement
24+
visibility: False
25+
condition: boolean
26+
value: 'True'
27+
support_level: TECHNOLOGY_PREVIEW
28+
description: The feature flag represents enabling IPv6 only traffic to be allowed through the gateway component and does not include all components of the platform.
29+
support_url: https://access.redhat.com/articles/7116569
30+
labels:
31+
- gateway
32+
- name: FEATURE_GATEWAY_CREATE_CRC_SERVICE_TYPE_ENABLED
33+
ui_name: Dynamic Service Type Feature
34+
visibility: False
35+
condition: boolean
36+
value: 'False'
37+
support_level: DEVELOPER_PREVIEW
38+
description: The Dynamic Service Type feature allows for the introduction of new platform services without requiring registration to the existing database. The new service can be enabled through the use of configuration.
39+
support_url: https://access.redhat.com/articles/7122668
40+
toggle_type: install-time
41+
labels:
42+
- gateway
43+
- name: FEATURE_DISPATCHERD_ENABLED
44+
ui_name: AAP Dispatcherd background tasking system
45+
visibility: False
46+
condition: boolean
47+
value: 'False'
48+
support_level: TECHNOLOGY_PREVIEW
49+
description: A service to run python tasks in subprocesses, designed specifically to work well with pg_notify, but intended to be extensible to other message delivery means.
50+
support_url: ''
51+
toggle_type: install-time
52+
labels:
53+
- eda
54+
- controller
55+
- name: FEATURE_CASE_INSENSITIVE_AUTH_MAPS_ENABLED
56+
ui_name: Case Insensitive Authentication Maps
57+
visibility: False
58+
condition: boolean
59+
value: 'True'
60+
support_level: DEVELOPER_PREVIEW
61+
description: Enable case-insensitive comparison for authentication mapping attributes and group names.
62+
support_url: ''
63+
labels:
64+
- platform
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Feature Flag Configuration Schema",
4+
"description": "Validates a list of feature flag configurations.",
5+
"type": "array",
6+
"items": {
7+
"type": "object",
8+
"properties": {
9+
"name": {
10+
"description": "The unique identifier for the feature flag. Must be in all capitals and start with 'FEATURE_' and end with '_ENABLED'",
11+
"type": "string",
12+
"pattern": "^FEATURE_[A-Z0-9_]+_ENABLED$"
13+
},
14+
"ui_name": {
15+
"description": "The human-readable name for the feature flag displayed in the UI.",
16+
"type": "string",
17+
"minLength": 1
18+
},
19+
"visibility": {
20+
"description": "Controls whether the feature is visible in the UI.",
21+
"type": "boolean"
22+
},
23+
"condition": {
24+
"description": "The type of condition for the feature flag's value. Currently only boolean is supported.",
25+
"type": "string",
26+
"enum": ["boolean"]
27+
},
28+
"value": {
29+
"description": "The default value of the feature flag, as a string.",
30+
"type": "string",
31+
"enum": ["True", "False"]
32+
},
33+
"support_level": {
34+
"description": "The level of support provided for this feature.",
35+
"type": "string",
36+
"enum": [
37+
"TECHNOLOGY_PREVIEW",
38+
"DEVELOPER_PREVIEW"
39+
]
40+
},
41+
"description": {
42+
"description": "A brief explanation of what the feature does.",
43+
"type": "string"
44+
},
45+
"support_url": {
46+
"description": "A URL to the relevant documentation for the feature.",
47+
"type": "string",
48+
"format": "uri"
49+
},
50+
"toggle_type": {
51+
"description": "The actual value of the feature flag. Note: The YAML string 'False' or 'True' is parsed as a boolean.",
52+
"type": "string",
53+
"enum": ["install-time", "run-time"]
54+
},
55+
"labels": {
56+
"description": "A list of labels to categorize the feature.",
57+
"type": "array",
58+
"items": {
59+
"type": "string",
60+
"enum": ["controller", "eda", "gateway", "platform"]
61+
},
62+
"minItems": 1,
63+
"uniqueItems": true
64+
}
65+
},
66+
"required": [
67+
"name",
68+
"ui_name",
69+
"visibility",
70+
"condition",
71+
"value",
72+
"support_level",
73+
"description",
74+
"support_url"
75+
]
76+
}
77+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.apps import apps
2+
from flags.sources import Condition
3+
4+
5+
class DatabaseCondition(Condition):
6+
"""Condition that includes the AAPFlags database object
7+
This is required to ensure that enable_flag/disable_flag calls
8+
can work as expected, with the custom flag objects
9+
"""
10+
11+
def __init__(self, condition, value, required=False, obj=None):
12+
super().__init__(condition, value, required=required)
13+
self.obj = obj
14+
15+
16+
class AAPFlagSource(object):
17+
"""The customer AAP flag source, retrieves a list of all flags in the database"""
18+
19+
def get_queryset(self):
20+
aap_flags = apps.get_model('dab_feature_flags', 'AAPFlag')
21+
return aap_flags.objects.all()
22+
23+
def get_flags(self):
24+
flags = {}
25+
for o in self.get_queryset():
26+
if o.name not in flags:
27+
flags[o.name] = []
28+
flags[o.name].append(DatabaseCondition(o.condition, o.value, required=o.required, obj=o))
29+
return flags

ansible_base/feature_flags/management/__init__.py

Whitespace-only changes.

ansible_base/feature_flags/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
try:
2+
from tabulate import tabulate
3+
4+
HAS_TABULATE = True
5+
except ImportError:
6+
HAS_TABULATE = False
7+
8+
from django.core.management.base import BaseCommand
9+
from flags.state import flag_state
10+
11+
from ansible_base.feature_flags.models import AAPFlag
12+
13+
14+
class Command(BaseCommand):
15+
help = "AAP Feature Flag management command"
16+
17+
def add_arguments(self, parser):
18+
parser.add_argument("--list", action="store_true", help="List feature flags", required=False)
19+
20+
def handle(self, *args, **options):
21+
if options["list"]:
22+
self.list_feature_flags()
23+
24+
def list_feature_flags(self):
25+
feature_flags = []
26+
headers = ["Name", "UI_Name", "Value", "State", "Support Level", "Visibility", "Toggle Type", "Description", "Support URL"]
27+
28+
for feature_flag in AAPFlag.objects.all().order_by('name'):
29+
feature_flags.append(
30+
[
31+
f'{feature_flag.name}',
32+
f'{feature_flag.ui_name}',
33+
f'{feature_flag.value}',
34+
f'{flag_state(feature_flag.name)}',
35+
f'{feature_flag.support_level}',
36+
f'{feature_flag.visibility}',
37+
f'{feature_flag.toggle_type}',
38+
f'{feature_flag.description}',
39+
f'{feature_flag.support_url}',
40+
]
41+
)
42+
self.stdout.write('')
43+
44+
if HAS_TABULATE:
45+
self.stdout.write(tabulate(feature_flags, headers, tablefmt="github"))
46+
else:
47+
self.stdout.write("\t".join(headers))
48+
for feature_flag in feature_flags:
49+
self.stdout.write("\t".join(feature_flag))
50+
self.stdout.write('')
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 4.2.21 on 2025-06-24 13:34
2+
# FileHash: 8d069529c20fce63ddd45711b014cbb6603be30c2d45108b10881480c49cfbcc
3+
4+
import ansible_base.feature_flags.models.aap_flag
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
initial = True
13+
14+
dependencies = [
15+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='AAPFlag',
21+
fields=[
22+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23+
('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')),
24+
('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')),
25+
('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])),
26+
('ui_name', models.CharField(help_text='The pretty name to display in the application User Interface', max_length=64)),
27+
('condition', models.CharField(default='boolean', help_text='Used to specify a condition, which if met, will enable the feature flag.', max_length=64)),
28+
('value', models.CharField(default='False', help_text='The value used to evaluate the conditional specified.', max_length=127)),
29+
('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.")),
30+
('support_level', models.CharField(choices=[('DEVELOPER_PREVIEW', 'Developer Preview'), ('TECHNOLOGY_PREVIEW', 'Technology Preview')], editable=False, help_text='The support criteria for the feature flag. Must be one of (DEVELOPER_PREVIEW or TECHNOLOGY_PREVIEW).', max_length=25)),
31+
('visibility', models.BooleanField(default=False, help_text='Controls whether the feature is visible in the UI.')),
32+
('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)),
33+
('description', models.CharField(default='', help_text='A detailed description giving an overview of the feature flag.', max_length=500)),
34+
('support_url', models.CharField(blank=True, default='', help_text='A link to the documentation support URL for the feature', max_length=250)),
35+
('labels', models.JSONField(blank=True, default=list, help_text='A list of labels for the feature flag.', null=True, validators=[ansible_base.feature_flags.models.aap_flag.validate_labels])),
36+
('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)),
37+
('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)),
38+
],
39+
options={
40+
'unique_together': {('name', 'condition')},
41+
},
42+
),
43+
]

0 commit comments

Comments
 (0)