Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/workflows/sonarcloud_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +155 to +156
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlanCoding could you help me understand why sonar fails?
test is there but it seems coveragerc is making sonar blind

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
14 changes: 6 additions & 8 deletions awx/main/tests/functional/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
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
Expand Down Expand Up @@ -302,13 +299,14 @@ def test_undefined_function_cannot_be_imported(self):
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
Expand Down
29 changes: 28 additions & 1 deletion awx/main/tests/unit/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
'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',
)


Expand All @@ -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():
Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions awx/main/tests/unit/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ def test_overwritten_jt_extra_vars(self, job, private_data_dir, mock_me):


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()
Expand Down Expand Up @@ -545,6 +546,7 @@ def test_survey_extra_vars(self, mock_me):
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
Expand Down Expand Up @@ -845,6 +847,7 @@ def test_multi_vault_password_ask(self, private_data_dir, job, mock_me):
[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
Expand Down Expand Up @@ -901,6 +904,7 @@ def test_multi_cloud(self, private_data_dir, mock_me):

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()
Expand Down
20 changes: 17 additions & 3 deletions awx/resource_api.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -15,7 +21,11 @@ class APIConfig(ServiceAPIConfig):
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),
Expand All @@ -25,4 +35,8 @@ class APIConfig(ServiceAPIConfig):
RoleDefinition,
shared_resource=SharedResource(serializer=RoleDefinitionType, is_provider=False),
),
ResourceConfig(
AAPFlag,
shared_resource=SharedResource(serializer=FeatureFlagType, is_provider=False),
),
)
8 changes: 0 additions & 8 deletions awx/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
load_envvars,
load_python_file_with_injected_context,
load_standard_settings_files,
toggle_feature_flags,
)
from .functions import (
assert_production_settings,
Expand Down Expand Up @@ -71,12 +70,5 @@
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)
7 changes: 2 additions & 5 deletions awx/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,11 +1148,8 @@
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
Expand Down
12 changes: 2 additions & 10 deletions awx/settings/development_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
# /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']

Expand Down Expand Up @@ -70,11 +68,5 @@
# 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
10 changes: 5 additions & 5 deletions requirements/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading