mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 22:16:00 -03:30
Move to Runtime Platform Flags (#16148)
* move to platform flags Signed-off-by: Fabricio Aguiar <fabricio.aguiar@gmail.com> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED * SonarCloud analyzes files without coverage data.
This commit is contained in:
24
.github/workflows/sonarcloud_pr.yml
vendored
24
.github/workflows/sonarcloud_pr.yml
vendored
@@ -152,11 +152,27 @@ jobs:
|
||||
echo "All changed files in PR:"
|
||||
echo "$files"
|
||||
|
||||
# Convert to comma-separated list for sonar.inclusions
|
||||
# Filter out files that are excluded by .coveragerc to avoid coverage conflicts
|
||||
# This prevents SonarCloud from analyzing files that have no coverage data
|
||||
if [ -n "$files" ]; then
|
||||
inclusions=$(echo "$files" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
|
||||
echo "└── Result: ✅ Will scan these files: $inclusions"
|
||||
# Filter out files matching .coveragerc omit patterns
|
||||
filtered_files=$(echo "$files" | grep -v "settings/.*_defaults\.py$" | grep -v "settings/defaults\.py$" | grep -v "main/migrations/")
|
||||
|
||||
# Show which files were filtered out for transparency
|
||||
excluded_files=$(echo "$files" | grep -E "(settings/.*_defaults\.py$|settings/defaults\.py$|main/migrations/)" || true)
|
||||
if [ -n "$excluded_files" ]; then
|
||||
echo "├── Filtered out (coverage-excluded): $(echo "$excluded_files" | wc -l) file(s)"
|
||||
echo "$excluded_files" | sed 's/^/│ - /'
|
||||
fi
|
||||
|
||||
if [ -n "$filtered_files" ]; then
|
||||
inclusions=$(echo "$filtered_files" | tr '\n' ',' | sed 's/,$//')
|
||||
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
|
||||
echo "└── Result: ✅ Will scan these files (excluding coverage-omitted files): $inclusions"
|
||||
else
|
||||
echo "└── Result: ✅ All changed files are excluded by coverage config, running full SonarCloud analysis"
|
||||
# Don't set SONAR_INCLUSIONS, let it scan everything per sonar-project.properties
|
||||
fi
|
||||
else
|
||||
echo "└── Result: ✅ Running SonarCloud analysis"
|
||||
fi
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
|
||||
from flags.state import get_flags, flag_state
|
||||
from ansible_base.feature_flags.models import AAPFlag
|
||||
from ansible_base.feature_flags.utils import create_initial_data as seed_feature_flags
|
||||
from django.conf import settings
|
||||
from awx.main.models import User
|
||||
|
||||
|
||||
@override_settings(FLAGS={})
|
||||
@pytest.mark.django_db
|
||||
def test_feature_flags_list_endpoint(get):
|
||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=False)
|
||||
|
||||
url = "/api/v2/feature_flags_state/"
|
||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=True)
|
||||
url = "/api/v2/feature_flags/states/"
|
||||
response = get(url, user=bob, expect=200)
|
||||
assert len(response.data) == 0
|
||||
assert len(get_flags()) > 0
|
||||
assert len(response.data["results"]) == len(get_flags())
|
||||
|
||||
|
||||
@override_settings(
|
||||
FLAGS={
|
||||
"FEATURE_SOME_PLATFORM_FLAG_ENABLED": [
|
||||
{"condition": "boolean", "value": False},
|
||||
{"condition": "before date", "value": "2022-06-01T12:00Z"},
|
||||
],
|
||||
"FEATURE_SOME_PLATFORM_FLAG_FOO_ENABLED": [
|
||||
{"condition": "boolean", "value": True},
|
||||
],
|
||||
}
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_feature_flags_list_endpoint_override(get):
|
||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=False)
|
||||
@pytest.mark.parametrize('flag_val', (True, False))
|
||||
def test_feature_flags_list_endpoint_override(get, flag_val):
|
||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=True)
|
||||
|
||||
url = "/api/v2/feature_flags_state/"
|
||||
AAPFlag.objects.all().delete()
|
||||
flag_name = "FEATURE_DISPATCHERD_ENABLED"
|
||||
setattr(settings, flag_name, flag_val)
|
||||
seed_feature_flags()
|
||||
url = "/api/v2/feature_flags/states/"
|
||||
response = get(url, user=bob, expect=200)
|
||||
assert len(response.data) == 2
|
||||
assert response.data["FEATURE_SOME_PLATFORM_FLAG_ENABLED"] is False
|
||||
assert response.data["FEATURE_SOME_PLATFORM_FLAG_FOO_ENABLED"] is True
|
||||
assert len(response.data["results"]) == 6
|
||||
assert flag_state(flag_name) == flag_val
|
||||
|
||||
@@ -5,11 +5,8 @@ import signal
|
||||
import time
|
||||
import yaml
|
||||
from unittest import mock
|
||||
from copy import deepcopy
|
||||
|
||||
from flags.state import disable_flag, enable_flag
|
||||
from django.utils.timezone import now as tz_now
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
import pytest
|
||||
|
||||
from awx.main.models import Job, WorkflowJob, Instance
|
||||
@@ -302,13 +299,14 @@ class TestTaskDispatcher:
|
||||
assert str(result) == "No module named 'awx.foo'" # noqa
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTaskPublisher:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _disable_dispatcherd(self):
|
||||
ffs = deepcopy(settings.FLAGS)
|
||||
ffs['FEATURE_DISPATCHERD_ENABLED'][0]['value'] = False
|
||||
with override_settings(FLAGS=ffs):
|
||||
yield
|
||||
flag_name = "FEATURE_DISPATCHERD_ENABLED"
|
||||
disable_flag(flag_name)
|
||||
yield
|
||||
enable_flag(flag_name)
|
||||
|
||||
def test_function_callable(self):
|
||||
assert add(2, 2) == 4
|
||||
|
||||
@@ -9,6 +9,9 @@ LOCAL_SETTINGS = (
|
||||
'DEBUG',
|
||||
'NAMED_URL_GRAPH',
|
||||
'DISPATCHER_MOCK_PUBLISH',
|
||||
# Platform flags are managed by the platform flags system and have environment-specific defaults
|
||||
'FEATURE_DISPATCHERD_ENABLED',
|
||||
'FEATURE_INDIRECT_NODE_COUNTING_ENABLED',
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +31,7 @@ def test_default_settings():
|
||||
continue
|
||||
default_val = getattr(settings.default_settings, k, None)
|
||||
snapshot_val = settings.DEFAULTS_SNAPSHOT[k]
|
||||
assert default_val == snapshot_val, f'Setting for {k} does not match shapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}'
|
||||
assert default_val == snapshot_val, f'Setting for {k} does not match snapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}'
|
||||
|
||||
|
||||
def test_django_conf_settings_is_awx_settings():
|
||||
@@ -69,3 +72,27 @@ def test_merge_application_name():
|
||||
result = merge_application_name(settings)["DATABASES__default__OPTIONS__application_name"]
|
||||
assert result.startswith("awx-")
|
||||
assert "test-cluster" in result
|
||||
|
||||
|
||||
def test_development_defaults_feature_flags(monkeypatch):
|
||||
"""Ensure that development_defaults.py sets the correct feature flags."""
|
||||
monkeypatch.setenv('AWX_MODE', 'development')
|
||||
|
||||
# Import the development_defaults module directly to trigger coverage of the new lines
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
spec = importlib.util.spec_from_file_location("development_defaults", os.path.join(os.path.dirname(__file__), "../../../settings/development_defaults.py"))
|
||||
development_defaults = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(development_defaults)
|
||||
|
||||
# Also import through the development settings to ensure both paths are tested
|
||||
from awx.settings.development import FEATURE_INDIRECT_NODE_COUNTING_ENABLED, FEATURE_DISPATCHERD_ENABLED
|
||||
|
||||
# Verify the feature flags are set correctly in both the module and settings
|
||||
assert hasattr(development_defaults, 'FEATURE_INDIRECT_NODE_COUNTING_ENABLED')
|
||||
assert development_defaults.FEATURE_INDIRECT_NODE_COUNTING_ENABLED is True
|
||||
assert hasattr(development_defaults, 'FEATURE_DISPATCHERD_ENABLED')
|
||||
assert development_defaults.FEATURE_DISPATCHERD_ENABLED is True
|
||||
assert FEATURE_INDIRECT_NODE_COUNTING_ENABLED is True
|
||||
assert FEATURE_DISPATCHERD_ENABLED is True
|
||||
|
||||
@@ -461,6 +461,7 @@ class TestExtraVarSanitation(TestJobExecution):
|
||||
|
||||
|
||||
class TestGenericRun:
|
||||
@pytest.mark.django_db(reset_sequences=True)
|
||||
def test_generic_failure(self, patch_Job, execution_environment, mock_me, mock_create_partition):
|
||||
job = Job(status='running', inventory=Inventory(), project=Project(local_path='/projects/_23_foo'))
|
||||
job.websocket_emit_status = mock.Mock()
|
||||
@@ -545,6 +546,7 @@ class TestGenericRun:
|
||||
private_data_dir, extra_vars, safe_dict = call_args
|
||||
assert extra_vars['super_secret'] == "CLASSIFIED"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_awx_task_env(self, patch_Job, private_data_dir, execution_environment, mock_me):
|
||||
job = Job(project=Project(), inventory=Inventory())
|
||||
job.execution_environment = execution_environment
|
||||
@@ -845,6 +847,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
[None, '0'],
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_net_credentials(self, authorize, expected_authorize, job, private_data_dir, mock_me):
|
||||
task = jobs.RunJob()
|
||||
task.instance = job
|
||||
@@ -901,6 +904,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
|
||||
assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_awx_task_env(self, settings, private_data_dir, job, mock_me):
|
||||
settings.AWX_TASK_ENV = {'FOO': 'BAR'}
|
||||
task = jobs.RunJob()
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from ansible_base.resource_registry.registry import ParentResource, ResourceConfig, ServiceAPIConfig, SharedResource
|
||||
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
|
||||
from ansible_base.rbac.models import RoleDefinition
|
||||
from ansible_base.resource_registry.shared_types import RoleDefinitionType
|
||||
|
||||
from ansible_base.resource_registry.shared_types import (
|
||||
FeatureFlagType,
|
||||
RoleDefinitionType,
|
||||
OrganizationType,
|
||||
TeamType,
|
||||
UserType,
|
||||
)
|
||||
from ansible_base.feature_flags.models import AAPFlag
|
||||
from awx.main import models
|
||||
|
||||
|
||||
@@ -15,7 +21,11 @@ RESOURCE_LIST = (
|
||||
models.Organization,
|
||||
shared_resource=SharedResource(serializer=OrganizationType, is_provider=False),
|
||||
),
|
||||
ResourceConfig(models.User, shared_resource=SharedResource(serializer=UserType, is_provider=False), name_field="username"),
|
||||
ResourceConfig(
|
||||
models.User,
|
||||
shared_resource=SharedResource(serializer=UserType, is_provider=False),
|
||||
name_field="username",
|
||||
),
|
||||
ResourceConfig(
|
||||
models.Team,
|
||||
shared_resource=SharedResource(serializer=TeamType, is_provider=False),
|
||||
@@ -25,4 +35,8 @@ RESOURCE_LIST = (
|
||||
RoleDefinition,
|
||||
shared_resource=SharedResource(serializer=RoleDefinitionType, is_provider=False),
|
||||
),
|
||||
ResourceConfig(
|
||||
AAPFlag,
|
||||
shared_resource=SharedResource(serializer=FeatureFlagType, is_provider=False),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from ansible_base.lib.dynamic_config import (
|
||||
load_envvars,
|
||||
load_python_file_with_injected_context,
|
||||
load_standard_settings_files,
|
||||
toggle_feature_flags,
|
||||
)
|
||||
from .functions import (
|
||||
assert_production_settings,
|
||||
@@ -71,12 +70,5 @@ DYNACONF.update(
|
||||
merge=True,
|
||||
)
|
||||
|
||||
# Toggle feature flags based on installer settings
|
||||
DYNACONF.update(
|
||||
toggle_feature_flags(DYNACONF),
|
||||
loader_identifier="awx.settings:toggle_feature_flags",
|
||||
merge=True,
|
||||
)
|
||||
|
||||
# Update django.conf.settings with DYNACONF values
|
||||
export(__name__, DYNACONF)
|
||||
|
||||
@@ -1148,11 +1148,8 @@ OPA_REQUEST_TIMEOUT = 1.5 # The number of seconds after which the connection to
|
||||
OPA_REQUEST_RETRIES = 2 # The number of retry attempts for connecting to the OPA server. Default is 2.
|
||||
|
||||
# feature flags
|
||||
FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',)
|
||||
FLAGS = {
|
||||
'FEATURE_INDIRECT_NODE_COUNTING_ENABLED': [{'condition': 'boolean', 'value': False}],
|
||||
'FEATURE_DISPATCHERD_ENABLED': [{'condition': 'boolean', 'value': False}],
|
||||
}
|
||||
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = False
|
||||
FEATURE_DISPATCHERD_ENABLED = False
|
||||
|
||||
# Dispatcher worker lifetime. If set to None, workers will never be retired
|
||||
# based on age. Note workers will finish their last task before retiring if
|
||||
|
||||
@@ -11,8 +11,6 @@ import socket
|
||||
# /usr/lib64/python/mimetypes.py
|
||||
import mimetypes
|
||||
|
||||
from dynaconf import post_hook
|
||||
|
||||
# awx-manage shell_plus --notebook
|
||||
NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser']
|
||||
|
||||
@@ -70,11 +68,5 @@ AWX_DISABLE_TASK_MANAGERS = False
|
||||
# Needed for launching runserver in debug mode
|
||||
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
|
||||
|
||||
|
||||
# This modifies FLAGS set by defaults, must be deferred to run later
|
||||
@post_hook
|
||||
def set_dev_flags(settings):
|
||||
defaults_flags = settings.get("FLAGS", {})
|
||||
defaults_flags['FEATURE_INDIRECT_NODE_COUNTING_ENABLED'] = [{'condition': 'boolean', 'value': True}]
|
||||
defaults_flags['FEATURE_DISPATCHERD_ENABLED'] = [{'condition': 'boolean', 'value': True}]
|
||||
return {'FLAGS': defaults_flags}
|
||||
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = True
|
||||
FEATURE_DISPATCHERD_ENABLED = True
|
||||
|
||||
@@ -22,7 +22,7 @@ ansi2html==1.9.2
|
||||
# via -r /awx_devel/requirements/requirements_git.txt
|
||||
asciichartpy==1.5.25
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
asgiref==3.10.0
|
||||
asgiref==3.11.0
|
||||
# via
|
||||
# channels
|
||||
# channels-redis
|
||||
@@ -104,7 +104,7 @@ click==8.1.8
|
||||
# via receptorctl
|
||||
constantly==23.10.4
|
||||
# via twisted
|
||||
cryptography==46.0.2
|
||||
cryptography==46.0.3
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# adal
|
||||
@@ -138,7 +138,7 @@ django==4.2.26
|
||||
# django-solo
|
||||
# djangorestframework
|
||||
# drf-spectacular
|
||||
# django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel # git requirements installed separately
|
||||
# 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
|
||||
# via -r /awx_devel/requirements/requirements_git.txt
|
||||
django-cors-headers==4.9.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
@@ -148,7 +148,7 @@ django-crum==0.7.9
|
||||
# django-ansible-base
|
||||
django-extensions==4.1
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
django-flags==5.0.14
|
||||
django-flags==5.1.0
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# django-ansible-base
|
||||
@@ -169,7 +169,7 @@ drf-spectacular==0.29.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
durationpy==0.10
|
||||
# via kubernetes
|
||||
dynaconf==3.2.11
|
||||
dynaconf==3.2.12
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# django-ansible-base
|
||||
|
||||
Reference in New Issue
Block a user