mirror of
https://github.com/ansible/awx.git
synced 2026-03-17 08:57:33 -02: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 "All changed files in PR:"
|
||||||
echo "$files"
|
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
|
if [ -n "$files" ]; then
|
||||||
inclusions=$(echo "$files" | tr '\n' ',' | sed 's/,$//')
|
# Filter out files matching .coveragerc omit patterns
|
||||||
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
|
filtered_files=$(echo "$files" | grep -v "settings/.*_defaults\.py$" | grep -v "settings/defaults\.py$" | grep -v "main/migrations/")
|
||||||
echo "└── Result: ✅ Will scan these files: $inclusions"
|
|
||||||
|
# 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
|
else
|
||||||
echo "└── Result: ✅ Running SonarCloud analysis"
|
echo "└── Result: ✅ Running SonarCloud analysis"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
import pytest
|
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
|
from awx.main.models import User
|
||||||
|
|
||||||
|
|
||||||
@override_settings(FLAGS={})
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_feature_flags_list_endpoint(get):
|
def test_feature_flags_list_endpoint(get):
|
||||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=False)
|
bob = User.objects.create(username='bob', password='test_user', is_superuser=True)
|
||||||
|
url = "/api/v2/feature_flags/states/"
|
||||||
url = "/api/v2/feature_flags_state/"
|
|
||||||
response = get(url, user=bob, expect=200)
|
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
|
@pytest.mark.django_db
|
||||||
def test_feature_flags_list_endpoint_override(get):
|
@pytest.mark.parametrize('flag_val', (True, False))
|
||||||
bob = User.objects.create(username='bob', password='test_user', is_superuser=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)
|
response = get(url, user=bob, expect=200)
|
||||||
assert len(response.data) == 2
|
assert len(response.data["results"]) == 6
|
||||||
assert response.data["FEATURE_SOME_PLATFORM_FLAG_ENABLED"] is False
|
assert flag_state(flag_name) == flag_val
|
||||||
assert response.data["FEATURE_SOME_PLATFORM_FLAG_FOO_ENABLED"] is True
|
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import signal
|
|||||||
import time
|
import time
|
||||||
import yaml
|
import yaml
|
||||||
from unittest import mock
|
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.utils.timezone import now as tz_now
|
||||||
from django.conf import settings
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.main.models import Job, WorkflowJob, Instance
|
from awx.main.models import Job, WorkflowJob, Instance
|
||||||
@@ -302,13 +299,14 @@ class TestTaskDispatcher:
|
|||||||
assert str(result) == "No module named 'awx.foo'" # noqa
|
assert str(result) == "No module named 'awx.foo'" # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
class TestTaskPublisher:
|
class TestTaskPublisher:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _disable_dispatcherd(self):
|
def _disable_dispatcherd(self):
|
||||||
ffs = deepcopy(settings.FLAGS)
|
flag_name = "FEATURE_DISPATCHERD_ENABLED"
|
||||||
ffs['FEATURE_DISPATCHERD_ENABLED'][0]['value'] = False
|
disable_flag(flag_name)
|
||||||
with override_settings(FLAGS=ffs):
|
yield
|
||||||
yield
|
enable_flag(flag_name)
|
||||||
|
|
||||||
def test_function_callable(self):
|
def test_function_callable(self):
|
||||||
assert add(2, 2) == 4
|
assert add(2, 2) == 4
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ LOCAL_SETTINGS = (
|
|||||||
'DEBUG',
|
'DEBUG',
|
||||||
'NAMED_URL_GRAPH',
|
'NAMED_URL_GRAPH',
|
||||||
'DISPATCHER_MOCK_PUBLISH',
|
'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
|
continue
|
||||||
default_val = getattr(settings.default_settings, k, None)
|
default_val = getattr(settings.default_settings, k, None)
|
||||||
snapshot_val = settings.DEFAULTS_SNAPSHOT[k]
|
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():
|
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"]
|
result = merge_application_name(settings)["DATABASES__default__OPTIONS__application_name"]
|
||||||
assert result.startswith("awx-")
|
assert result.startswith("awx-")
|
||||||
assert "test-cluster" in result
|
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:
|
class TestGenericRun:
|
||||||
|
@pytest.mark.django_db(reset_sequences=True)
|
||||||
def test_generic_failure(self, patch_Job, execution_environment, mock_me, mock_create_partition):
|
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 = Job(status='running', inventory=Inventory(), project=Project(local_path='/projects/_23_foo'))
|
||||||
job.websocket_emit_status = mock.Mock()
|
job.websocket_emit_status = mock.Mock()
|
||||||
@@ -545,6 +546,7 @@ class TestGenericRun:
|
|||||||
private_data_dir, extra_vars, safe_dict = call_args
|
private_data_dir, extra_vars, safe_dict = call_args
|
||||||
assert extra_vars['super_secret'] == "CLASSIFIED"
|
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):
|
def test_awx_task_env(self, patch_Job, private_data_dir, execution_environment, mock_me):
|
||||||
job = Job(project=Project(), inventory=Inventory())
|
job = Job(project=Project(), inventory=Inventory())
|
||||||
job.execution_environment = execution_environment
|
job.execution_environment = execution_environment
|
||||||
@@ -845,6 +847,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
[None, '0'],
|
[None, '0'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
def test_net_credentials(self, authorize, expected_authorize, job, private_data_dir, mock_me):
|
def test_net_credentials(self, authorize, expected_authorize, job, private_data_dir, mock_me):
|
||||||
task = jobs.RunJob()
|
task = jobs.RunJob()
|
||||||
task.instance = job
|
task.instance = job
|
||||||
@@ -901,6 +904,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
|
|
||||||
assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD
|
assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
def test_awx_task_env(self, settings, private_data_dir, job, mock_me):
|
def test_awx_task_env(self, settings, private_data_dir, job, mock_me):
|
||||||
settings.AWX_TASK_ENV = {'FOO': 'BAR'}
|
settings.AWX_TASK_ENV = {'FOO': 'BAR'}
|
||||||
task = jobs.RunJob()
|
task = jobs.RunJob()
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
from ansible_base.resource_registry.registry import ParentResource, ResourceConfig, ServiceAPIConfig, SharedResource
|
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.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
|
from awx.main import models
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +21,11 @@ RESOURCE_LIST = (
|
|||||||
models.Organization,
|
models.Organization,
|
||||||
shared_resource=SharedResource(serializer=OrganizationType, is_provider=False),
|
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(
|
ResourceConfig(
|
||||||
models.Team,
|
models.Team,
|
||||||
shared_resource=SharedResource(serializer=TeamType, is_provider=False),
|
shared_resource=SharedResource(serializer=TeamType, is_provider=False),
|
||||||
@@ -25,4 +35,8 @@ RESOURCE_LIST = (
|
|||||||
RoleDefinition,
|
RoleDefinition,
|
||||||
shared_resource=SharedResource(serializer=RoleDefinitionType, is_provider=False),
|
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_envvars,
|
||||||
load_python_file_with_injected_context,
|
load_python_file_with_injected_context,
|
||||||
load_standard_settings_files,
|
load_standard_settings_files,
|
||||||
toggle_feature_flags,
|
|
||||||
)
|
)
|
||||||
from .functions import (
|
from .functions import (
|
||||||
assert_production_settings,
|
assert_production_settings,
|
||||||
@@ -71,12 +70,5 @@ DYNACONF.update(
|
|||||||
merge=True,
|
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
|
# Update django.conf.settings with DYNACONF values
|
||||||
export(__name__, DYNACONF)
|
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.
|
OPA_REQUEST_RETRIES = 2 # The number of retry attempts for connecting to the OPA server. Default is 2.
|
||||||
|
|
||||||
# feature flags
|
# feature flags
|
||||||
FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',)
|
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = False
|
||||||
FLAGS = {
|
FEATURE_DISPATCHERD_ENABLED = False
|
||||||
'FEATURE_INDIRECT_NODE_COUNTING_ENABLED': [{'condition': 'boolean', 'value': False}],
|
|
||||||
'FEATURE_DISPATCHERD_ENABLED': [{'condition': 'boolean', 'value': False}],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Dispatcher worker lifetime. If set to None, workers will never be retired
|
# 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
|
# based on age. Note workers will finish their last task before retiring if
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import socket
|
|||||||
# /usr/lib64/python/mimetypes.py
|
# /usr/lib64/python/mimetypes.py
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
from dynaconf import post_hook
|
|
||||||
|
|
||||||
# awx-manage shell_plus --notebook
|
# awx-manage shell_plus --notebook
|
||||||
NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser']
|
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
|
# Needed for launching runserver in debug mode
|
||||||
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
|
# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!=================================
|
||||||
|
|
||||||
|
FEATURE_INDIRECT_NODE_COUNTING_ENABLED = True
|
||||||
# This modifies FLAGS set by defaults, must be deferred to run later
|
FEATURE_DISPATCHERD_ENABLED = True
|
||||||
@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}
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ansi2html==1.9.2
|
|||||||
# via -r /awx_devel/requirements/requirements_git.txt
|
# via -r /awx_devel/requirements/requirements_git.txt
|
||||||
asciichartpy==1.5.25
|
asciichartpy==1.5.25
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
asgiref==3.10.0
|
asgiref==3.11.0
|
||||||
# via
|
# via
|
||||||
# channels
|
# channels
|
||||||
# channels-redis
|
# channels-redis
|
||||||
@@ -104,7 +104,7 @@ click==8.1.8
|
|||||||
# via receptorctl
|
# via receptorctl
|
||||||
constantly==23.10.4
|
constantly==23.10.4
|
||||||
# via twisted
|
# via twisted
|
||||||
cryptography==46.0.2
|
cryptography==46.0.3
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
# -r /awx_devel/requirements/requirements.in
|
||||||
# adal
|
# adal
|
||||||
@@ -138,7 +138,7 @@ django==4.2.26
|
|||||||
# django-solo
|
# django-solo
|
||||||
# djangorestframework
|
# djangorestframework
|
||||||
# drf-spectacular
|
# 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
|
# via -r /awx_devel/requirements/requirements_git.txt
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==4.9.0
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
@@ -148,7 +148,7 @@ django-crum==0.7.9
|
|||||||
# django-ansible-base
|
# django-ansible-base
|
||||||
django-extensions==4.1
|
django-extensions==4.1
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
django-flags==5.0.14
|
django-flags==5.1.0
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
# -r /awx_devel/requirements/requirements.in
|
||||||
# django-ansible-base
|
# django-ansible-base
|
||||||
@@ -169,7 +169,7 @@ drf-spectacular==0.29.0
|
|||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
durationpy==0.10
|
durationpy==0.10
|
||||||
# via kubernetes
|
# via kubernetes
|
||||||
dynaconf==3.2.11
|
dynaconf==3.2.12
|
||||||
# via
|
# via
|
||||||
# -r /awx_devel/requirements/requirements.in
|
# -r /awx_devel/requirements/requirements.in
|
||||||
# django-ansible-base
|
# django-ansible-base
|
||||||
|
|||||||
Reference in New Issue
Block a user