Skip to content

Commit 2b2f2b7

Browse files
authored
Move to Runtime Platform Flags (#16148)
* move to platform flags Signed-off-by: Fabricio Aguiar <[email protected]> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED * SonarCloud analyzes files without coverage data.
1 parent e03beb4 commit 2b2f2b7

File tree

10 files changed

+102
-68
lines changed

10 files changed

+102
-68
lines changed

.github/workflows/sonarcloud_pr.yml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,27 @@ jobs:
152152
echo "All changed files in PR:"
153153
echo "$files"
154154
155-
# Convert to comma-separated list for sonar.inclusions
155+
# Filter out files that are excluded by .coveragerc to avoid coverage conflicts
156+
# This prevents SonarCloud from analyzing files that have no coverage data
156157
if [ -n "$files" ]; then
157-
inclusions=$(echo "$files" | tr '\n' ',' | sed 's/,$//')
158-
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
159-
echo "└── Result: ✅ Will scan these files: $inclusions"
158+
# Filter out files matching .coveragerc omit patterns
159+
filtered_files=$(echo "$files" | grep -v "settings/.*_defaults\.py$" | grep -v "settings/defaults\.py$" | grep -v "main/migrations/")
160+
161+
# Show which files were filtered out for transparency
162+
excluded_files=$(echo "$files" | grep -E "(settings/.*_defaults\.py$|settings/defaults\.py$|main/migrations/)" || true)
163+
if [ -n "$excluded_files" ]; then
164+
echo "├── Filtered out (coverage-excluded): $(echo "$excluded_files" | wc -l) file(s)"
165+
echo "$excluded_files" | sed 's/^/│ - /'
166+
fi
167+
168+
if [ -n "$filtered_files" ]; then
169+
inclusions=$(echo "$filtered_files" | tr '\n' ',' | sed 's/,$//')
170+
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
171+
echo "└── Result: ✅ Will scan these files (excluding coverage-omitted files): $inclusions"
172+
else
173+
echo "└── Result: ✅ All changed files are excluded by coverage config, running full SonarCloud analysis"
174+
# Don't set SONAR_INCLUSIONS, let it scan everything per sonar-project.properties
175+
fi
160176
else
161177
echo "└── Result: ✅ Running SonarCloud analysis"
162178
fi
Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,30 @@
11
import pytest
2-
from django.test import override_settings
3-
2+
from flags.state import get_flags, flag_state
3+
from ansible_base.feature_flags.models import AAPFlag
4+
from ansible_base.feature_flags.utils import create_initial_data as seed_feature_flags
5+
from django.conf import settings
46
from awx.main.models import User
57

68

7-
@override_settings(FLAGS={})
89
@pytest.mark.django_db
910
def test_feature_flags_list_endpoint(get):
10-
bob = User.objects.create(username='bob', password='test_user', is_superuser=False)
11-
12-
url = "/api/v2/feature_flags_state/"
11+
bob = User.objects.create(username='bob', password='test_user', is_superuser=True)
12+
url = "/api/v2/feature_flags/states/"
1313
response = get(url, user=bob, expect=200)
14-
assert len(response.data) == 0
14+
assert len(get_flags()) > 0
15+
assert len(response.data["results"]) == len(get_flags())
1516

1617

17-
@override_settings(
18-
FLAGS={
19-
"FEATURE_SOME_PLATFORM_FLAG_ENABLED": [
20-
{"condition": "boolean", "value": False},
21-
{"condition": "before date", "value": "2022-06-01T12:00Z"},
22-
],
23-
"FEATURE_SOME_PLATFORM_FLAG_FOO_ENABLED": [
24-
{"condition": "boolean", "value": True},
25-
],
26-
}
27-
)
2818
@pytest.mark.django_db
29-
def test_feature_flags_list_endpoint_override(get):
30-
bob = User.objects.create(username='bob', password='test_user', is_superuser=False)
19+
@pytest.mark.parametrize('flag_val', (True, False))
20+
def test_feature_flags_list_endpoint_override(get, flag_val):
21+
bob = User.objects.create(username='bob', password='test_user', is_superuser=True)
3122

32-
url = "/api/v2/feature_flags_state/"
23+
AAPFlag.objects.all().delete()
24+
flag_name = "FEATURE_DISPATCHERD_ENABLED"
25+
setattr(settings, flag_name, flag_val)
26+
seed_feature_flags()
27+
url = "/api/v2/feature_flags/states/"
3328
response = get(url, user=bob, expect=200)
34-
assert len(response.data) == 2
35-
assert response.data["FEATURE_SOME_PLATFORM_FLAG_ENABLED"] is False
36-
assert response.data["FEATURE_SOME_PLATFORM_FLAG_FOO_ENABLED"] is True
29+
assert len(response.data["results"]) == 6
30+
assert flag_state(flag_name) == flag_val

awx/main/tests/functional/test_dispatch.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
import time
66
import yaml
77
from unittest import mock
8-
from copy import deepcopy
9-
8+
from flags.state import disable_flag, enable_flag
109
from django.utils.timezone import now as tz_now
11-
from django.conf import settings
12-
from django.test.utils import override_settings
1310
import pytest
1411

1512
from awx.main.models import Job, WorkflowJob, Instance
@@ -302,13 +299,14 @@ def test_undefined_function_cannot_be_imported(self):
302299
assert str(result) == "No module named 'awx.foo'" # noqa
303300

304301

302+
@pytest.mark.django_db
305303
class TestTaskPublisher:
306304
@pytest.fixture(autouse=True)
307305
def _disable_dispatcherd(self):
308-
ffs = deepcopy(settings.FLAGS)
309-
ffs['FEATURE_DISPATCHERD_ENABLED'][0]['value'] = False
310-
with override_settings(FLAGS=ffs):
311-
yield
306+
flag_name = "FEATURE_DISPATCHERD_ENABLED"
307+
disable_flag(flag_name)
308+
yield
309+
enable_flag(flag_name)
312310

313311
def test_function_callable(self):
314312
assert add(2, 2) == 4

awx/main/tests/unit/test_settings.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
'DEBUG',
1010
'NAMED_URL_GRAPH',
1111
'DISPATCHER_MOCK_PUBLISH',
12+
# Platform flags are managed by the platform flags system and have environment-specific defaults
13+
'FEATURE_DISPATCHERD_ENABLED',
14+
'FEATURE_INDIRECT_NODE_COUNTING_ENABLED',
1215
)
1316

1417

@@ -28,7 +31,7 @@ def test_default_settings():
2831
continue
2932
default_val = getattr(settings.default_settings, k, None)
3033
snapshot_val = settings.DEFAULTS_SNAPSHOT[k]
31-
assert default_val == snapshot_val, f'Setting for {k} does not match shapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}'
34+
assert default_val == snapshot_val, f'Setting for {k} does not match snapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}'
3235

3336

3437
def test_django_conf_settings_is_awx_settings():
@@ -69,3 +72,27 @@ def test_merge_application_name():
6972
result = merge_application_name(settings)["DATABASES__default__OPTIONS__application_name"]
7073
assert result.startswith("awx-")
7174
assert "test-cluster" in result
75+
76+
77+
def test_development_defaults_feature_flags(monkeypatch):
78+
"""Ensure that development_defaults.py sets the correct feature flags."""
79+
monkeypatch.setenv('AWX_MODE', 'development')
80+
81+
# Import the development_defaults module directly to trigger coverage of the new lines
82+
import importlib.util
83+
import os
84+
85+
spec = importlib.util.spec_from_file_location("development_defaults", os.path.join(os.path.dirname(__file__), "../../../settings/development_defaults.py"))
86+
development_defaults = importlib.util.module_from_spec(spec)
87+
spec.loader.exec_module(development_defaults)
88+
89+
# Also import through the development settings to ensure both paths are tested
90+
from awx.settings.development import FEATURE_INDIRECT_NODE_COUNTING_ENABLED, FEATURE_DISPATCHERD_ENABLED
91+
92+
# Verify the feature flags are set correctly in both the module and settings
93+
assert hasattr(development_defaults, 'FEATURE_INDIRECT_NODE_COUNTING_ENABLED')
94+
assert development_defaults.FEATURE_INDIRECT_NODE_COUNTING_ENABLED is True
95+
assert hasattr(development_defaults, 'FEATURE_DISPATCHERD_ENABLED')
96+
assert development_defaults.FEATURE_DISPATCHERD_ENABLED is True
97+
assert FEATURE_INDIRECT_NODE_COUNTING_ENABLED is True
98+
assert FEATURE_DISPATCHERD_ENABLED is True

awx/main/tests/unit/test_tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ def test_overwritten_jt_extra_vars(self, job, private_data_dir, mock_me):
461461

462462

463463
class TestGenericRun:
464+
@pytest.mark.django_db(reset_sequences=True)
464465
def test_generic_failure(self, patch_Job, execution_environment, mock_me, mock_create_partition):
465466
job = Job(status='running', inventory=Inventory(), project=Project(local_path='/projects/_23_foo'))
466467
job.websocket_emit_status = mock.Mock()
@@ -545,6 +546,7 @@ def test_survey_extra_vars(self, mock_me):
545546
private_data_dir, extra_vars, safe_dict = call_args
546547
assert extra_vars['super_secret'] == "CLASSIFIED"
547548

549+
@pytest.mark.django_db
548550
def test_awx_task_env(self, patch_Job, private_data_dir, execution_environment, mock_me):
549551
job = Job(project=Project(), inventory=Inventory())
550552
job.execution_environment = execution_environment
@@ -845,6 +847,7 @@ def test_multi_vault_password_ask(self, private_data_dir, job, mock_me):
845847
[None, '0'],
846848
],
847849
)
850+
@pytest.mark.django_db
848851
def test_net_credentials(self, authorize, expected_authorize, job, private_data_dir, mock_me):
849852
task = jobs.RunJob()
850853
task.instance = job
@@ -901,6 +904,7 @@ def test_multi_cloud(self, private_data_dir, mock_me):
901904

902905
assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD
903906

907+
@pytest.mark.django_db
904908
def test_awx_task_env(self, settings, private_data_dir, job, mock_me):
905909
settings.AWX_TASK_ENV = {'FOO': 'BAR'}
906910
task = jobs.RunJob()

awx/resource_api.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from ansible_base.resource_registry.registry import ParentResource, ResourceConfig, ServiceAPIConfig, SharedResource
2-
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
32
from ansible_base.rbac.models import RoleDefinition
4-
from ansible_base.resource_registry.shared_types import RoleDefinitionType
53

4+
from ansible_base.resource_registry.shared_types import (
5+
FeatureFlagType,
6+
RoleDefinitionType,
7+
OrganizationType,
8+
TeamType,
9+
UserType,
10+
)
11+
from ansible_base.feature_flags.models import AAPFlag
612
from awx.main import models
713

814

@@ -15,7 +21,11 @@ class APIConfig(ServiceAPIConfig):
1521
models.Organization,
1622
shared_resource=SharedResource(serializer=OrganizationType, is_provider=False),
1723
),
18-
ResourceConfig(models.User, shared_resource=SharedResource(serializer=UserType, is_provider=False), name_field="username"),
24+
ResourceConfig(
25+
models.User,
26+
shared_resource=SharedResource(serializer=UserType, is_provider=False),
27+
name_field="username",
28+
),
1929
ResourceConfig(
2030
models.Team,
2131
shared_resource=SharedResource(serializer=TeamType, is_provider=False),
@@ -25,4 +35,8 @@ class APIConfig(ServiceAPIConfig):
2535
RoleDefinition,
2636
shared_resource=SharedResource(serializer=RoleDefinitionType, is_provider=False),
2737
),
38+
ResourceConfig(
39+
AAPFlag,
40+
shared_resource=SharedResource(serializer=FeatureFlagType, is_provider=False),
41+
),
2842
)

awx/settings/__init__.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
load_envvars,
99
load_python_file_with_injected_context,
1010
load_standard_settings_files,
11-
toggle_feature_flags,
1211
)
1312
from .functions import (
1413
assert_production_settings,
@@ -71,12 +70,5 @@
7170
merge=True,
7271
)
7372

74-
# Toggle feature flags based on installer settings
75-
DYNACONF.update(
76-
toggle_feature_flags(DYNACONF),
77-
loader_identifier="awx.settings:toggle_feature_flags",
78-
merge=True,
79-
)
80-
8173
# Update django.conf.settings with DYNACONF values
8274
export(__name__, DYNACONF)

awx/settings/defaults.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,11 +1148,8 @@
11481148
OPA_REQUEST_RETRIES = 2 # The number of retry attempts for connecting to the OPA server. Default is 2.
11491149

11501150
# feature flags
1151-
FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',)
1152-
FLAGS = {
1153-
'FEATURE_INDIRECT_NODE_COUNTING_ENABLED': [{'condition': 'boolean', 'value': False}],
1154-
'FEATURE_DISPATCHERD_ENABLED': [{'condition': 'boolean', 'value': False}],
1155-
}
1151+
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = False
1152+
FEATURE_DISPATCHERD_ENABLED = False
11561153

11571154
# Dispatcher worker lifetime. If set to None, workers will never be retired
11581155
# based on age. Note workers will finish their last task before retiring if

awx/settings/development_defaults.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
# /usr/lib64/python/mimetypes.py
1212
import mimetypes
1313

14-
from dynaconf import post_hook
15-
1614
# awx-manage shell_plus --notebook
1715
NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser']
1816

@@ -70,11 +68,5 @@
7068
# Needed for launching runserver in debug mode
7169
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
7270

73-
74-
# This modifies FLAGS set by defaults, must be deferred to run later
75-
@post_hook
76-
def set_dev_flags(settings):
77-
defaults_flags = settings.get("FLAGS", {})
78-
defaults_flags['FEATURE_INDIRECT_NODE_COUNTING_ENABLED'] = [{'condition': 'boolean', 'value': True}]
79-
defaults_flags['FEATURE_DISPATCHERD_ENABLED'] = [{'condition': 'boolean', 'value': True}]
80-
return {'FLAGS': defaults_flags}
71+
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = True
72+
FEATURE_DISPATCHERD_ENABLED = True

requirements/requirements.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ ansi2html==1.9.2
2222
# via -r /awx_devel/requirements/requirements_git.txt
2323
asciichartpy==1.5.25
2424
# via -r /awx_devel/requirements/requirements.in
25-
asgiref==3.10.0
25+
asgiref==3.11.0
2626
# via
2727
# channels
2828
# channels-redis
@@ -104,7 +104,7 @@ click==8.1.8
104104
# via receptorctl
105105
constantly==23.10.4
106106
# via twisted
107-
cryptography==46.0.2
107+
cryptography==46.0.3
108108
# via
109109
# -r /awx_devel/requirements/requirements.in
110110
# adal
@@ -138,7 +138,7 @@ django==4.2.26
138138
# django-solo
139139
# djangorestframework
140140
# drf-spectacular
141-
# django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel # git requirements installed separately
141+
# django-ansible-base[feature-flags,jwt-consumer,rbac,resource-registry,rest-filters] @ git+https://github.com/ansible/django-ansible-base@devel # git requirements installed separately
142142
# via -r /awx_devel/requirements/requirements_git.txt
143143
django-cors-headers==4.9.0
144144
# via -r /awx_devel/requirements/requirements.in
@@ -148,7 +148,7 @@ django-crum==0.7.9
148148
# django-ansible-base
149149
django-extensions==4.1
150150
# via -r /awx_devel/requirements/requirements.in
151-
django-flags==5.0.14
151+
django-flags==5.1.0
152152
# via
153153
# -r /awx_devel/requirements/requirements.in
154154
# django-ansible-base
@@ -169,7 +169,7 @@ drf-spectacular==0.29.0
169169
# via -r /awx_devel/requirements/requirements.in
170170
durationpy==0.10
171171
# via kubernetes
172-
dynaconf==3.2.11
172+
dynaconf==3.2.12
173173
# via
174174
# -r /awx_devel/requirements/requirements.in
175175
# django-ansible-base

0 commit comments

Comments
 (0)