Merge branch 'ansible:devel' into devel

This commit is contained in:
djyasin
2022-07-13 09:53:22 -04:00
committed by GitHub
130 changed files with 24020 additions and 19856 deletions

View File

@@ -29,12 +29,24 @@ In the future, sometimes starting a discussion on the development list prior to
Thank you once again for this and your interest in AWX! Thank you once again for this and your interest in AWX!
### No Progress ### No Progress Issue
- Hi! \
\
Thank you very much for for this issue. It means a lot to us that you have taken time to contribute by opening this report. \
\
On this issue, there were comments added but it has been some time since then without response. At this time we are closing this issue. If you get time to address the comments we can reopen the issue if you can contact us by using any of the communication methods listed in the page below: \
\
https://github.com/ansible/awx/#get-involved \
\
Thank you once again for this and your interest in AWX!
### No Progress PR
- Hi! \ - Hi! \
\ \
Thank you very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \ Thank you very much for your submission to AWX. It means a lot to us that you have taken time to contribute. \
\ \
On this PR, changes were requested but it has been some time since then. We think this PR has merit but without the requested changes we are unable to merge it. At this time we are closing you PR. If you get time to address the changes you are welcome to open another PR or we can reopen this PR upon request if you contact us by using any of the communication methods listed in the page below: \ On this PR, changes were requested but it has been some time since then. We think this PR has merit but without the requested changes we are unable to merge it. At this time we are closing your PR. If you get time to address the changes you are welcome to open another PR or we can reopen this PR upon request if you contact us by using any of the communication methods listed in the page below: \
\ \
https://github.com/ansible/awx/#get-involved \ https://github.com/ansible/awx/#get-involved \
\ \
@@ -51,6 +63,10 @@ Thank you once again for this and your interest in AWX!
### Code of Conduct ### Code of Conduct
- Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html - Hello. Please keep in mind that Ansible adheres to a Code of Conduct in its community spaces. The spirit of the code of conduct is to be kind, and this is your friendly reminder to be so. Please see the full code of conduct here if you have questions: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
### EE Contents / Community General
- Hello. The awx-ee contains the collections and dependencies needed for supported AWX features to function. Anything beyond that (like the community.general package) will require you to build your own EE. For information on how to do that, see https://ansible-builder.readthedocs.io/en/stable/ \
\
The Ansible Community is looking at building an EE that corresponds to all of the collections inside the ansible package. That may help you if and when it happens; see https://github.com/ansible-community/community-topics/issues/31 for details.

View File

@@ -113,7 +113,7 @@ jobs:
- name: Install playbook dependencies - name: Install playbook dependencies
run: | run: |
python3 -m pip install docker python3 -m pip install docker setuptools_scm
- name: Build AWX image - name: Build AWX image
working-directory: awx working-directory: awx

View File

@@ -21,7 +21,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python${{ env.py_version }} -m pip install wheel twine python${{ env.py_version }} -m pip install wheel twine setuptools-scm
- name: Set official collection namespace - name: Set official collection namespace
run: echo collection_namespace=awx >> $GITHUB_ENV run: echo collection_namespace=awx >> $GITHUB_ENV

View File

@@ -65,7 +65,7 @@ jobs:
- name: Install playbook dependencies - name: Install playbook dependencies
run: | run: |
python3 -m pip install docker python3 -m pip install docker setuptools_scm
- name: Build and stage AWX - name: Build and stage AWX
working-directory: awx working-directory: awx

View File

@@ -5,8 +5,8 @@ NPM_BIN ?= npm
CHROMIUM_BIN=/tmp/chrome-linux/chrome CHROMIUM_BIN=/tmp/chrome-linux/chrome
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
MANAGEMENT_COMMAND ?= awx-manage MANAGEMENT_COMMAND ?= awx-manage
VERSION := $(shell $(PYTHON) setup.py --version) VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
COLLECTION_VERSION := $(shell $(PYTHON) setup.py --version | cut -d . -f 1-3) COLLECTION_VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
# NOTE: This defaults the container image version to the branch that's active # NOTE: This defaults the container image version to the branch that's active
COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_TAG ?= $(GIT_BRANCH)
@@ -49,7 +49,7 @@ I18N_FLAG_FILE = .i18n_built
.PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \ .PHONY: awx-link clean clean-tmp clean-venv requirements requirements_dev \
develop refresh adduser migrate dbchange \ develop refresh adduser migrate dbchange \
receiver test test_unit test_coverage coverage_html \ receiver test test_unit test_coverage coverage_html \
dev_build release_build sdist \ sdist \
ui-release ui-devel \ ui-release ui-devel \
VERSION PYTHON_VERSION docker-compose-sources \ VERSION PYTHON_VERSION docker-compose-sources \
.git/hooks/pre-commit .git/hooks/pre-commit
@@ -273,7 +273,7 @@ api-lint:
yamllint -s . yamllint -s .
awx-link: awx-link:
[ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/setup.py egg_info_dev [ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/tools/scripts/egg_info_dev
cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/$(PYTHON)/site-packages/awx.egg-link cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/$(PYTHON)/site-packages/awx.egg-link
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
@@ -424,21 +424,13 @@ ui-test-general:
$(NPM_BIN) run --prefix awx/ui pretest $(NPM_BIN) run --prefix awx/ui pretest
$(NPM_BIN) run --prefix awx/ui/ test-general --runInBand $(NPM_BIN) run --prefix awx/ui/ test-general --runInBand
# Build a pip-installable package into dist/ with a timestamped version number.
dev_build:
$(PYTHON) setup.py dev_build
# Build a pip-installable package into dist/ with the release version number.
release_build:
$(PYTHON) setup.py release_build
HEADLESS ?= no HEADLESS ?= no
ifeq ($(HEADLESS), yes) ifeq ($(HEADLESS), yes)
dist/$(SDIST_TAR_FILE): dist/$(SDIST_TAR_FILE):
else else
dist/$(SDIST_TAR_FILE): $(UI_BUILD_FLAG_FILE) dist/$(SDIST_TAR_FILE): $(UI_BUILD_FLAG_FILE)
endif endif
$(PYTHON) setup.py $(SDIST_COMMAND) $(PYTHON) -m build -s
ln -sf $(SDIST_TAR_FILE) dist/awx.tar.gz ln -sf $(SDIST_TAR_FILE) dist/awx.tar.gz
sdist: dist/$(SDIST_TAR_FILE) sdist: dist/$(SDIST_TAR_FILE)

View File

@@ -6,9 +6,40 @@ import os
import sys import sys
import warnings import warnings
from pkg_resources import get_distribution
__version__ = get_distribution('awx').version def get_version():
version_from_file = get_version_from_file()
if version_from_file:
return version_from_file
else:
from setuptools_scm import get_version
version = get_version(root='..', relative_to=__file__)
return version
def get_version_from_file():
vf = version_file()
if vf:
with open(vf, 'r') as file:
return file.read().strip()
def version_file():
current_dir = os.path.dirname(os.path.abspath(__file__))
version_file = os.path.join(current_dir, '..', 'VERSION')
if os.path.exists(version_file):
return version_file
try:
import pkg_resources
__version__ = pkg_resources.get_distribution('awx').version
except pkg_resources.DistributionNotFound:
__version__ = get_version()
__all__ = ['__version__'] __all__ = ['__version__']
@@ -21,7 +52,6 @@ try:
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
MODE = 'production' MODE = 'production'
import hashlib import hashlib
try: try:

View File

@@ -2073,7 +2073,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
class Meta: class Meta:
model = InventorySource model = InventorySource
fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project', 'update_on_project_update') + ( fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project') + (
'last_update_failed', 'last_update_failed',
'last_updated', 'last_updated',
) # Backwards compatibility. ) # Backwards compatibility.
@@ -2136,11 +2136,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
return value return value
def validate_update_on_project_update(self, value):
if value and self.instance and self.instance.schedules.exists():
raise serializers.ValidationError(_("Setting not compatible with existing schedules."))
return value
def validate_inventory(self, value): def validate_inventory(self, value):
if value and value.kind == 'smart': if value and value.kind == 'smart':
raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")}) raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")})
@@ -2191,7 +2186,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None: if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None:
raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")}) raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")})
else: else:
redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path', 'update_on_project_update'])) redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path']))
if redundant_scm_fields: if redundant_scm_fields:
raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))}) raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))})
@@ -4745,13 +4740,6 @@ class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSeria
raise serializers.ValidationError(_('Inventory Source must be a cloud resource.')) raise serializers.ValidationError(_('Inventory Source must be a cloud resource.'))
elif type(value) == Project and value.scm_type == '': elif type(value) == Project and value.scm_type == '':
raise serializers.ValidationError(_('Manual Project cannot have a schedule set.')) raise serializers.ValidationError(_('Manual Project cannot have a schedule set.'))
elif type(value) == InventorySource and value.source == 'scm' and value.update_on_project_update:
raise serializers.ValidationError(
_(
'Inventory sources with `update_on_project_update` cannot be scheduled. '
'Schedule its source project `{}` instead.'.format(value.source_project.name)
)
)
return value return value
def validate(self, attrs): def validate(self, attrs):

View File

@@ -115,7 +115,6 @@ from awx.api.metadata import RoleMetadata
from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING
from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import ( from awx.api.views.mixin import (
ControlledByScmMixin,
InstanceGroupMembershipMixin, InstanceGroupMembershipMixin,
OrganizationCountsMixin, OrganizationCountsMixin,
RelatedJobsPreventDeleteMixin, RelatedJobsPreventDeleteMixin,
@@ -1675,7 +1674,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView):
return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST) return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST)
class HostDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
always_allow_superuser = False always_allow_superuser = False
model = models.Host model = models.Host
@@ -1709,7 +1708,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie
return qs return qs
class HostGroupsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView): class HostGroupsList(SubListCreateAttachDetachAPIView):
'''the list of groups a host is directly a member of''' '''the list of groups a host is directly a member of'''
model = models.Group model = models.Group
@@ -1825,7 +1824,7 @@ class EnforceParentRelationshipMixin(object):
return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs) return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs)
class GroupChildrenList(ControlledByScmMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
model = models.Group model = models.Group
serializer_class = serializers.GroupSerializer serializer_class = serializers.GroupSerializer
@@ -1871,7 +1870,7 @@ class GroupPotentialChildrenList(SubListAPIView):
return qs.exclude(pk__in=except_pks) return qs.exclude(pk__in=except_pks)
class GroupHostsList(HostRelatedSearchMixin, ControlledByScmMixin, SubListCreateAttachDetachAPIView): class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
'''the list of hosts directly below a group''' '''the list of hosts directly below a group'''
model = models.Host model = models.Host
@@ -1935,7 +1934,7 @@ class GroupActivityStreamList(SubListAPIView):
return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all())) return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all()))
class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class GroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.Group model = models.Group
serializer_class = serializers.GroupSerializer serializer_class = serializers.GroupSerializer

View File

@@ -41,7 +41,7 @@ from awx.api.serializers import (
JobTemplateSerializer, JobTemplateSerializer,
LabelSerializer, LabelSerializer,
) )
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin from awx.api.views.mixin import RelatedJobsPreventDeleteMixin
from awx.api.pagination import UnifiedJobEventPagination from awx.api.pagination import UnifiedJobEventPagination
@@ -75,7 +75,7 @@ class InventoryList(ListCreateAPIView):
serializer_class = InventorySerializer serializer_class = InventorySerializer
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = Inventory model = Inventory
serializer_class = InventorySerializer serializer_class = InventorySerializer

View File

@@ -10,13 +10,12 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.permissions import SAFE_METHODS
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from awx.main.constants import ACTIVE_STATES from awx.main.constants import ACTIVE_STATES
from awx.main.utils import get_object_or_400, parse_yaml_or_json from awx.main.utils import get_object_or_400
from awx.main.models.ha import Instance, InstanceGroup from awx.main.models.ha import Instance, InstanceGroup
from awx.main.models.organization import Team from awx.main.models.organization import Team
from awx.main.models.projects import Project from awx.main.models.projects import Project
@@ -186,35 +185,6 @@ class OrganizationCountsMixin(object):
return full_context return full_context
class ControlledByScmMixin(object):
"""
Special method to reset SCM inventory commit hash
if anything that it manages changes.
"""
def _reset_inv_src_rev(self, obj):
if self.request.method in SAFE_METHODS or not obj:
return
project_following_sources = obj.inventory_sources.filter(update_on_project_update=True, source='scm')
if project_following_sources:
# Allow inventory changes unrelated to variables
if self.model == Inventory and (
not self.request or not self.request.data or parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables)
):
return
project_following_sources.update(scm_last_revision='')
def get_object(self):
obj = super(ControlledByScmMixin, self).get_object()
self._reset_inv_src_rev(obj)
return obj
def get_parent_object(self):
obj = super(ControlledByScmMixin, self).get_parent_object()
self._reset_inv_src_rev(obj)
return obj
class NoTruncateMixin(object): class NoTruncateMixin(object):
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()

View File

@@ -129,7 +129,7 @@ def config(since, **kwargs):
} }
@register('counts', '1.1', description=_('Counts of objects such as organizations, inventories, and projects')) @register('counts', '1.2', description=_('Counts of objects such as organizations, inventories, and projects'))
def counts(since, **kwargs): def counts(since, **kwargs):
counts = {} counts = {}
for cls in ( for cls in (
@@ -172,6 +172,13 @@ def counts(since, **kwargs):
.count() .count()
) )
counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count() counts['pending_jobs'] = models.UnifiedJob.objects.exclude(launch_type='sync').filter(status__in=('pending',)).count()
if connection.vendor == 'postgresql':
with connection.cursor() as cursor:
cursor.execute(f"select count(*) from pg_stat_activity where datname=\'{connection.settings_dict['NAME']}\'")
counts['database_connections'] = cursor.fetchone()[0]
else:
# We should be using postgresql, but if we do that change that ever we should change the below value
counts['database_connections'] = 1
return counts return counts

View File

@@ -126,6 +126,8 @@ def metrics():
LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license', registry=REGISTRY) LICENSE_INSTANCE_TOTAL = Gauge('awx_license_instance_total', 'Total number of managed hosts provided by your license', registry=REGISTRY)
LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license', registry=REGISTRY) LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining managed hosts provided by your license', registry=REGISTRY)
DATABASE_CONNECTIONS = Gauge('awx_database_connections_total', 'Number of connections to database', registry=REGISTRY)
license_info = get_license() license_info = get_license()
SYSTEM_INFO.info( SYSTEM_INFO.info(
{ {
@@ -163,6 +165,8 @@ def metrics():
USER_SESSIONS.labels(type='user').set(current_counts['active_user_sessions']) USER_SESSIONS.labels(type='user').set(current_counts['active_user_sessions'])
USER_SESSIONS.labels(type='anonymous').set(current_counts['active_anonymous_sessions']) USER_SESSIONS.labels(type='anonymous').set(current_counts['active_anonymous_sessions'])
DATABASE_CONNECTIONS.set(current_counts['database_connections'])
all_job_data = job_counts(None) all_job_data = job_counts(None)
statuses = all_job_data.get('status', {}) statuses = all_job_data.get('status', {})
for status, value in statuses.items(): for status, value in statuses.items():

View File

@@ -10,6 +10,27 @@ from awx.main.models import Instance, UnifiedJob, WorkflowJob
logger = logging.getLogger('awx.main.dispatch') logger = logging.getLogger('awx.main.dispatch')
def startup_reaping():
"""
If this particular instance is starting, then we know that any running jobs are invalid
so we will reap those jobs as a special action here
"""
me = Instance.objects.me()
jobs = UnifiedJob.objects.filter(status='running', controller_node=me.hostname)
job_ids = []
for j in jobs:
job_ids.append(j.id)
j.status = 'failed'
j.start_args = ''
j.job_explanation += 'Task was marked as running at system start up. The system must have not shut down properly, so it has been marked as failed.'
j.save(update_fields=['status', 'start_args', 'job_explanation'])
if hasattr(j, 'send_notification_templates'):
j.send_notification_templates('failed')
j.websocket_emit_status('failed')
if job_ids:
logger.error(f'Unified jobs {job_ids} were reaped on dispatch startup')
def reap_job(j, status): def reap_job(j, status):
if UnifiedJob.objects.get(id=j.id).status not in ('running', 'waiting'): if UnifiedJob.objects.get(id=j.id).status not in ('running', 'waiting'):
# just in case, don't reap jobs that aren't running # just in case, don't reap jobs that aren't running

View File

@@ -169,8 +169,9 @@ class AWXConsumerPG(AWXConsumerBase):
logger.exception(f"Error consuming new events from postgres, will retry for {self.pg_max_wait} s") logger.exception(f"Error consuming new events from postgres, will retry for {self.pg_max_wait} s")
self.pg_down_time = time.time() self.pg_down_time = time.time()
self.pg_is_down = True self.pg_is_down = True
if time.time() - self.pg_down_time > self.pg_max_wait: current_downtime = time.time() - self.pg_down_time
logger.warning(f"Postgres event consumer has not recovered in {self.pg_max_wait} s, exiting") if current_downtime > self.pg_max_wait:
logger.exception(f"Postgres event consumer has not recovered in {current_downtime} s, exiting")
raise raise
# Wait for a second before next attempt, but still listen for any shutdown signals # Wait for a second before next attempt, but still listen for any shutdown signals
for i in range(10): for i in range(10):
@@ -179,6 +180,10 @@ class AWXConsumerPG(AWXConsumerBase):
time.sleep(0.1) time.sleep(0.1)
for conn in db.connections.all(): for conn in db.connections.all():
conn.close_if_unusable_or_obsolete() conn.close_if_unusable_or_obsolete()
except Exception:
# Log unanticipated exception in addition to writing to stderr to get timestamps and other metadata
logger.exception('Encountered unhandled error in dispatcher main loop')
raise
class BaseWorker(object): class BaseWorker(object):

View File

@@ -53,7 +53,7 @@ class Command(BaseCommand):
# (like the node heartbeat) # (like the node heartbeat)
periodic.run_continuously() periodic.run_continuously()
reaper.reap() reaper.startup_reaping()
consumer = None consumer = None
try: try:

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.2.13 on 2022-06-21 21:29
from django.db import migrations
import logging
logger = logging.getLogger("awx")
def forwards(apps, schema_editor):
InventorySource = apps.get_model('main', 'InventorySource')
sources = InventorySource.objects.filter(update_on_project_update=True)
for src in sources:
if src.update_on_launch == False:
src.update_on_launch = True
src.save(update_fields=['update_on_launch'])
logger.info(f"Setting update_on_launch to True for {src}")
proj = src.source_project
if proj and proj.scm_update_on_launch is False:
proj.scm_update_on_launch = True
proj.save(update_fields=['scm_update_on_launch'])
logger.warning(f"Setting scm_update_on_launch to True for {proj}")
class Migration(migrations.Migration):
dependencies = [
('main', '0163_convert_job_tags_to_textfield'),
]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
migrations.RemoveField(
model_name='inventorysource',
name='scm_last_revision',
),
migrations.RemoveField(
model_name='inventorysource',
name='update_on_project_update',
),
]

View File

@@ -35,6 +35,7 @@ def gce(cred, env, private_data_dir):
container_path = to_container_path(path, private_data_dir) container_path = to_container_path(path, private_data_dir)
env['GCE_CREDENTIALS_FILE_PATH'] = container_path env['GCE_CREDENTIALS_FILE_PATH'] = container_path
env['GCP_SERVICE_ACCOUNT_FILE'] = container_path env['GCP_SERVICE_ACCOUNT_FILE'] = container_path
env['GOOGLE_APPLICATION_CREDENTIALS'] = container_path
# Handle env variables for new module types. # Handle env variables for new module types.
# This includes gcp_compute inventory plugin and # This includes gcp_compute inventory plugin and

View File

@@ -985,22 +985,11 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
default=None, default=None,
null=True, null=True,
) )
scm_last_revision = models.CharField(
max_length=1024,
blank=True,
default='',
editable=False,
)
update_on_project_update = models.BooleanField(
default=False,
help_text=_(
'This field is deprecated and will be removed in a future release. '
'In future release, functionality will be migrated to source project update_on_launch.'
),
)
update_on_launch = models.BooleanField( update_on_launch = models.BooleanField(
default=False, default=False,
) )
update_cache_timeout = models.PositiveIntegerField( update_cache_timeout = models.PositiveIntegerField(
default=0, default=0,
) )
@@ -1038,14 +1027,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
self.name = 'inventory source (%s)' % replace_text self.name = 'inventory source (%s)' % replace_text
if 'name' not in update_fields: if 'name' not in update_fields:
update_fields.append('name') update_fields.append('name')
# Reset revision if SCM source has changed parameters
if self.source == 'scm' and not is_new_instance:
before_is = self.__class__.objects.get(pk=self.pk)
if before_is.source_path != self.source_path or before_is.source_project_id != self.source_project_id:
# Reset the scm_revision if file changed to force update
self.scm_last_revision = ''
if 'scm_last_revision' not in update_fields:
update_fields.append('scm_last_revision')
# Do the actual save. # Do the actual save.
super(InventorySource, self).save(*args, **kwargs) super(InventorySource, self).save(*args, **kwargs)
@@ -1054,10 +1035,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
if replace_text in self.name: if replace_text in self.name:
self.name = self.name.replace(replace_text, str(self.pk)) self.name = self.name.replace(replace_text, str(self.pk))
super(InventorySource, self).save(update_fields=['name']) super(InventorySource, self).save(update_fields=['name'])
if self.source == 'scm' and is_new_instance and self.update_on_project_update:
# Schedule a new Project update if one is not already queued
if self.source_project and not self.source_project.project_updates.filter(status__in=['new', 'pending', 'waiting']).exists():
self.update()
if not getattr(_inventory_updates, 'is_updating', False): if not getattr(_inventory_updates, 'is_updating', False):
if self.inventory is not None: if self.inventory is not None:
self.inventory.update_computed_fields() self.inventory.update_computed_fields()
@@ -1147,25 +1124,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
) )
return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates)) return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates))
def clean_update_on_project_update(self):
if (
self.update_on_project_update is True
and self.source == 'scm'
and InventorySource.objects.filter(Q(inventory=self.inventory, update_on_project_update=True, source='scm') & ~Q(id=self.id)).exists()
):
raise ValidationError(_("More than one SCM-based inventory source with update on project update per-inventory not allowed."))
return self.update_on_project_update
def clean_update_on_launch(self):
if self.update_on_project_update is True and self.source == 'scm' and self.update_on_launch is True:
raise ValidationError(
_(
"Cannot update SCM-based inventory source on launch if set to update on project update. "
"Instead, configure the corresponding source project to update on launch."
)
)
return self.update_on_launch
def clean_source_path(self): def clean_source_path(self):
if self.source != 'scm' and self.source_path: if self.source != 'scm' and self.source_path:
raise ValidationError(_("Cannot set source_path if not SCM type.")) raise ValidationError(_("Cannot set source_path if not SCM type."))
@@ -1301,13 +1259,6 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
return self.global_instance_groups return self.global_instance_groups
return selected_groups return selected_groups
def cancel(self, job_explanation=None, is_chain=False):
res = super(InventoryUpdate, self).cancel(job_explanation=job_explanation, is_chain=is_chain)
if res:
if self.launch_type != 'scm' and self.source_project_update:
self.source_project_update.cancel(job_explanation=job_explanation)
return res
class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
class Meta: class Meta:

View File

@@ -743,6 +743,12 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
return "$hidden due to Ansible no_log flag$" return "$hidden due to Ansible no_log flag$"
return artifacts return artifacts
def get_effective_artifacts(self, **kwargs):
"""Return unified job artifacts (from set_stats) to pass downstream in workflows"""
if isinstance(self.artifacts, dict):
return self.artifacts
return {}
@property @property
def is_container_group_task(self): def is_container_group_task(self):
return bool(self.instance_group and self.instance_group.is_container_group) return bool(self.instance_group and self.instance_group.is_container_group)

View File

@@ -533,7 +533,7 @@ class UnifiedJob(
('workflow', _('Workflow')), # Job was started from a workflow job. ('workflow', _('Workflow')), # Job was started from a workflow job.
('webhook', _('Webhook')), # Job was started from a webhook event. ('webhook', _('Webhook')), # Job was started from a webhook event.
('sync', _('Sync')), # Job was started from a project sync. ('sync', _('Sync')), # Job was started from a project sync.
('scm', _('SCM Update')), # Job was created as an Inventory SCM sync. ('scm', _('SCM Update')), # (deprecated) Job was created as an Inventory SCM sync.
] ]
PASSWORD_FIELDS = ('start_args',) PASSWORD_FIELDS = ('start_args',)
@@ -1204,6 +1204,10 @@ class UnifiedJob(
pass pass
return None return None
def get_effective_artifacts(self, **kwargs):
"""Return unified job artifacts (from set_stats) to pass downstream in workflows"""
return {}
def get_passwords_needed_to_start(self): def get_passwords_needed_to_start(self):
return [] return []

View File

@@ -318,8 +318,8 @@ class WorkflowJobNode(WorkflowNodeBase):
for parent_node in self.get_parent_nodes(): for parent_node in self.get_parent_nodes():
is_root_node = False is_root_node = False
aa_dict.update(parent_node.ancestor_artifacts) aa_dict.update(parent_node.ancestor_artifacts)
if parent_node.job and hasattr(parent_node.job, 'artifacts'): if parent_node.job:
aa_dict.update(parent_node.job.artifacts) aa_dict.update(parent_node.job.get_effective_artifacts(parents_set=set([self.workflow_job_id])))
if aa_dict and not is_root_node: if aa_dict and not is_root_node:
self.ancestor_artifacts = aa_dict self.ancestor_artifacts = aa_dict
self.save(update_fields=['ancestor_artifacts']) self.save(update_fields=['ancestor_artifacts'])
@@ -682,6 +682,27 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, SurveyJobMixin, JobNotificatio
wj = wj.get_workflow_job() wj = wj.get_workflow_job()
return ancestors return ancestors
def get_effective_artifacts(self, **kwargs):
"""
For downstream jobs of a workflow nested inside of a workflow,
we send aggregated artifacts from the nodes inside of the nested workflow
"""
artifacts = {}
job_queryset = (
UnifiedJob.objects.filter(unified_job_node__workflow_job=self)
.defer('job_args', 'job_cwd', 'start_args', 'result_traceback')
.order_by('finished', 'id')
.filter(status__in=['successful', 'failed'])
.iterator()
)
parents_set = kwargs.get('parents_set', set())
new_parents_set = parents_set | {self.id}
for job in job_queryset:
if job.id in parents_set:
continue
artifacts.update(job.get_effective_artifacts(parents_set=new_parents_set))
return artifacts
def get_notification_templates(self): def get_notification_templates(self):
return self.workflow_job_template.notification_templates return self.workflow_job_template.notification_templates

View File

@@ -248,11 +248,11 @@ class TaskManager:
workflow_job.save(update_fields=update_fields) workflow_job.save(update_fields=update_fields)
status_changed = True status_changed = True
if status_changed: if status_changed:
if workflow_job.spawned_by_workflow:
schedule_task_manager()
workflow_job.websocket_emit_status(workflow_job.status) workflow_job.websocket_emit_status(workflow_job.status)
# Operations whose queries rely on modifications made during the atomic scheduling session # Operations whose queries rely on modifications made during the atomic scheduling session
workflow_job.send_notification_templates('succeeded' if workflow_job.status == 'successful' else 'failed') workflow_job.send_notification_templates('succeeded' if workflow_job.status == 'successful' else 'failed')
if workflow_job.spawned_by_workflow:
schedule_task_manager()
return result return result
@timeit @timeit

View File

@@ -16,6 +16,7 @@ from awx.main.redact import UriCleaner
from awx.main.constants import MINIMAL_EVENTS, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE from awx.main.constants import MINIMAL_EVENTS, ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE
from awx.main.utils.update_model import update_model from awx.main.utils.update_model import update_model
from awx.main.queue import CallbackQueueDispatcher from awx.main.queue import CallbackQueueDispatcher
from awx.main.tasks.signals import signal_callback
logger = logging.getLogger('awx.main.tasks.callback') logger = logging.getLogger('awx.main.tasks.callback')
@@ -179,7 +180,13 @@ class RunnerCallback:
Ansible runner callback to tell the job when/if it is canceled Ansible runner callback to tell the job when/if it is canceled
""" """
unified_job_id = self.instance.pk unified_job_id = self.instance.pk
if signal_callback():
return True
try:
self.instance = self.update_model(unified_job_id) self.instance = self.update_model(unified_job_id)
except Exception:
logger.exception(f'Encountered error during cancel check for {unified_job_id}, canceling now')
return True
if not self.instance: if not self.instance:
logger.error('unified job {} was deleted while running, canceling'.format(unified_job_id)) logger.error('unified job {} was deleted while running, canceling'.format(unified_job_id))
return True return True

View File

@@ -19,7 +19,6 @@ from uuid import uuid4
# Django # Django
from django.conf import settings from django.conf import settings
from django.db import transaction
# Runner # Runner
@@ -34,7 +33,6 @@ from gitdb.exc import BadName as BadGitName
from awx.main.dispatch.publish import task from awx.main.dispatch.publish import task
from awx.main.dispatch import get_local_queuename from awx.main.dispatch import get_local_queuename
from awx.main.constants import ( from awx.main.constants import (
ACTIVE_STATES,
PRIVILEGE_ESCALATION_METHODS, PRIVILEGE_ESCALATION_METHODS,
STANDARD_INVENTORY_UPDATE_ENV, STANDARD_INVENTORY_UPDATE_ENV,
JOB_FOLDER_PREFIX, JOB_FOLDER_PREFIX,
@@ -64,6 +62,7 @@ from awx.main.tasks.callback import (
RunnerCallbackForProjectUpdate, RunnerCallbackForProjectUpdate,
RunnerCallbackForSystemJob, RunnerCallbackForSystemJob,
) )
from awx.main.tasks.signals import with_signal_handling, signal_callback
from awx.main.tasks.receptor import AWXReceptorJob from awx.main.tasks.receptor import AWXReceptorJob
from awx.main.exceptions import AwxTaskError, PostRunError, ReceptorNodeNotFound from awx.main.exceptions import AwxTaskError, PostRunError, ReceptorNodeNotFound
from awx.main.utils.ansible import read_ansible_config from awx.main.utils.ansible import read_ansible_config
@@ -394,6 +393,7 @@ class BaseTask(object):
instance.save(update_fields=['ansible_version']) instance.save(update_fields=['ansible_version'])
@with_path_cleanup @with_path_cleanup
@with_signal_handling
def run(self, pk, **kwargs): def run(self, pk, **kwargs):
""" """
Run the job/task and capture its output. Run the job/task and capture its output.
@@ -425,7 +425,7 @@ class BaseTask(object):
private_data_dir = self.build_private_data_dir(self.instance) private_data_dir = self.build_private_data_dir(self.instance)
self.pre_run_hook(self.instance, private_data_dir) self.pre_run_hook(self.instance, private_data_dir)
self.instance.log_lifecycle("preparing_playbook") self.instance.log_lifecycle("preparing_playbook")
if self.instance.cancel_flag: if self.instance.cancel_flag or signal_callback():
self.instance = self.update_model(self.instance.pk, status='canceled') self.instance = self.update_model(self.instance.pk, status='canceled')
if self.instance.status != 'running': if self.instance.status != 'running':
# Stop the task chain and prevent starting the job if it has # Stop the task chain and prevent starting the job if it has
@@ -547,6 +547,11 @@ class BaseTask(object):
self.runner_callback.delay_update(skip_if_already_set=True, job_explanation=f"Job terminated due to {status}") self.runner_callback.delay_update(skip_if_already_set=True, job_explanation=f"Job terminated due to {status}")
if status == 'timeout': if status == 'timeout':
status = 'failed' status = 'failed'
elif status == 'canceled':
self.instance = self.update_model(pk)
if (getattr(self.instance, 'cancel_flag', False) is False) and signal_callback():
self.runner_callback.delay_update(job_explanation="Task was canceled due to receiving a shutdown signal.")
status = 'failed'
except ReceptorNodeNotFound as exc: except ReceptorNodeNotFound as exc:
self.runner_callback.delay_update(job_explanation=str(exc)) self.runner_callback.delay_update(job_explanation=str(exc))
except Exception: except Exception:
@@ -1168,64 +1173,6 @@ class RunProjectUpdate(BaseTask):
d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes' d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes'
return d return d
def _update_dependent_inventories(self, project_update, dependent_inventory_sources):
scm_revision = project_update.project.scm_revision
inv_update_class = InventoryUpdate._get_task_class()
for inv_src in dependent_inventory_sources:
if not inv_src.update_on_project_update:
continue
if inv_src.scm_last_revision == scm_revision:
logger.debug('Skipping SCM inventory update for `{}` because ' 'project has not changed.'.format(inv_src.name))
continue
logger.debug('Local dependent inventory update for `{}`.'.format(inv_src.name))
with transaction.atomic():
if InventoryUpdate.objects.filter(inventory_source=inv_src, status__in=ACTIVE_STATES).exists():
logger.debug('Skipping SCM inventory update for `{}` because ' 'another update is already active.'.format(inv_src.name))
continue
if settings.IS_K8S:
instance_group = InventoryUpdate(inventory_source=inv_src).preferred_instance_groups[0]
else:
instance_group = project_update.instance_group
local_inv_update = inv_src.create_inventory_update(
_eager_fields=dict(
launch_type='scm',
status='running',
instance_group=instance_group,
execution_node=project_update.execution_node,
controller_node=project_update.execution_node,
source_project_update=project_update,
celery_task_id=project_update.celery_task_id,
)
)
local_inv_update.log_lifecycle("controller_node_chosen")
local_inv_update.log_lifecycle("execution_node_chosen")
try:
create_partition(local_inv_update.event_class._meta.db_table, start=local_inv_update.created)
inv_update_class().run(local_inv_update.id)
except Exception:
logger.exception('{} Unhandled exception updating dependent SCM inventory sources.'.format(project_update.log_format))
try:
project_update.refresh_from_db()
except ProjectUpdate.DoesNotExist:
logger.warning('Project update deleted during updates of dependent SCM inventory sources.')
break
try:
local_inv_update.refresh_from_db()
except InventoryUpdate.DoesNotExist:
logger.warning('%s Dependent inventory update deleted during execution.', project_update.log_format)
continue
if project_update.cancel_flag:
logger.info('Project update {} was canceled while updating dependent inventories.'.format(project_update.log_format))
break
if local_inv_update.cancel_flag:
logger.info('Continuing to process project dependencies after {} was canceled'.format(local_inv_update.log_format))
if local_inv_update.status == 'successful':
inv_src.scm_last_revision = scm_revision
inv_src.save(update_fields=['scm_last_revision'])
def release_lock(self, instance): def release_lock(self, instance):
try: try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN) fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
@@ -1435,12 +1382,6 @@ class RunProjectUpdate(BaseTask):
p.inventory_files = p.inventories p.inventory_files = p.inventories
p.save(update_fields=['scm_revision', 'playbook_files', 'inventory_files']) p.save(update_fields=['scm_revision', 'playbook_files', 'inventory_files'])
# Update any inventories that depend on this project
dependent_inventory_sources = p.scm_inventory_sources.filter(update_on_project_update=True)
if len(dependent_inventory_sources) > 0:
if status == 'successful' and instance.launch_type != 'sync':
self._update_dependent_inventories(instance, dependent_inventory_sources)
def build_execution_environment_params(self, instance, private_data_dir): def build_execution_environment_params(self, instance, private_data_dir):
if settings.IS_K8S: if settings.IS_K8S:
return {} return {}
@@ -1620,9 +1561,7 @@ class RunInventoryUpdate(BaseTask):
source_project = None source_project = None
if inventory_update.inventory_source: if inventory_update.inventory_source:
source_project = inventory_update.inventory_source.source_project source_project = inventory_update.inventory_source.source_project
if ( if inventory_update.source == 'scm' and source_project and source_project.scm_type: # never ever update manual projects
inventory_update.source == 'scm' and inventory_update.launch_type != 'scm' and source_project and source_project.scm_type
): # never ever update manual projects
# Check if the content cache exists, so that we do not unnecessarily re-download roles # Check if the content cache exists, so that we do not unnecessarily re-download roles
sync_needs = ['update_{}'.format(source_project.scm_type)] sync_needs = ['update_{}'.format(source_project.scm_type)]
@@ -1655,8 +1594,6 @@ class RunInventoryUpdate(BaseTask):
sync_task = project_update_task(job_private_data_dir=private_data_dir) sync_task = project_update_task(job_private_data_dir=private_data_dir)
sync_task.run(local_project_sync.id) sync_task.run(local_project_sync.id)
local_project_sync.refresh_from_db() local_project_sync.refresh_from_db()
inventory_update.inventory_source.scm_last_revision = local_project_sync.scm_revision
inventory_update.inventory_source.save(update_fields=['scm_last_revision'])
except Exception: except Exception:
inventory_update = self.update_model( inventory_update = self.update_model(
inventory_update.pk, inventory_update.pk,
@@ -1667,9 +1604,6 @@ class RunInventoryUpdate(BaseTask):
), ),
) )
raise raise
elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project:
# This follows update, not sync, so make copy here
RunProjectUpdate.make_local_copy(source_project, private_data_dir)
def post_run_hook(self, inventory_update, status): def post_run_hook(self, inventory_update, status):
super(RunInventoryUpdate, self).post_run_hook(inventory_update, status) super(RunInventoryUpdate, self).post_run_hook(inventory_update, status)

63
awx/main/tasks/signals.py Normal file
View File

@@ -0,0 +1,63 @@
import signal
import functools
import logging
logger = logging.getLogger('awx.main.tasks.signals')
__all__ = ['with_signal_handling', 'signal_callback']
class SignalState:
def reset(self):
self.sigterm_flag = False
self.is_active = False
self.original_sigterm = None
self.original_sigint = None
def __init__(self):
self.reset()
def set_flag(self, *args):
"""Method to pass into the python signal.signal method to receive signals"""
self.sigterm_flag = True
def connect_signals(self):
self.original_sigterm = signal.getsignal(signal.SIGTERM)
self.original_sigint = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGTERM, self.set_flag)
signal.signal(signal.SIGINT, self.set_flag)
self.is_active = True
def restore_signals(self):
signal.signal(signal.SIGTERM, self.original_sigterm)
signal.signal(signal.SIGINT, self.original_sigint)
self.reset()
signal_state = SignalState()
def signal_callback():
return signal_state.sigterm_flag
def with_signal_handling(f):
"""
Change signal handling to make signal_callback return True in event of SIGTERM or SIGINT.
"""
@functools.wraps(f)
def _wrapped(*args, **kwargs):
try:
this_is_outermost_caller = False
if not signal_state.is_active:
signal_state.connect_signals()
this_is_outermost_caller = True
return f(*args, **kwargs)
finally:
if this_is_outermost_caller:
signal_state.restore_signals()
return _wrapped

View File

@@ -114,10 +114,6 @@ def inform_cluster_of_shutdown():
try: try:
this_inst = Instance.objects.get(hostname=settings.CLUSTER_HOST_ID) this_inst = Instance.objects.get(hostname=settings.CLUSTER_HOST_ID)
this_inst.mark_offline(update_last_seen=True, errors=_('Instance received normal shutdown signal')) this_inst.mark_offline(update_last_seen=True, errors=_('Instance received normal shutdown signal'))
try:
reaper.reap(this_inst)
except Exception:
logger.exception('failed to reap jobs for {}'.format(this_inst.hostname))
logger.warning('Normal shutdown signal for instance {}, ' 'removed self from capacity pool.'.format(this_inst.hostname)) logger.warning('Normal shutdown signal for instance {}, ' 'removed self from capacity pool.'.format(this_inst.hostname))
except Exception: except Exception:
logger.exception('Encountered problem with normal shutdown signal.') logger.exception('Encountered problem with normal shutdown signal.')

View File

@@ -2,6 +2,7 @@
"ANSIBLE_JINJA2_NATIVE": "True", "ANSIBLE_JINJA2_NATIVE": "True",
"ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never", "ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS": "never",
"GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}", "GCE_CREDENTIALS_FILE_PATH": "{{ file_reference }}",
"GOOGLE_APPLICATION_CREDENTIALS": "{{ file_reference }}",
"GCP_AUTH_KIND": "serviceaccount", "GCP_AUTH_KIND": "serviceaccount",
"GCP_ENV_TYPE": "tower", "GCP_ENV_TYPE": "tower",
"GCP_PROJECT": "fooo", "GCP_PROJECT": "fooo",

View File

@@ -26,6 +26,7 @@ def test_empty():
"workflow_job_template": 0, "workflow_job_template": 0,
"unified_job": 0, "unified_job": 0,
"pending_jobs": 0, "pending_jobs": 0,
"database_connections": 1,
} }

View File

@@ -31,6 +31,7 @@ EXPECTED_VALUES = {
'awx_license_instance_total': 0, 'awx_license_instance_total': 0,
'awx_license_instance_free': 0, 'awx_license_instance_free': 0,
'awx_pending_jobs_total': 0, 'awx_pending_jobs_total': 0,
'awx_database_connections_total': 1,
} }

View File

@@ -9,9 +9,7 @@ from awx.api.versioning import reverse
@pytest.fixture @pytest.fixture
def ec2_source(inventory, project): def ec2_source(inventory, project):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
return inventory.inventory_sources.create( return inventory.inventory_sources.create(name='some_source', source='ec2', source_project=project)
name='some_source', update_on_project_update=True, source='ec2', source_project=project, scm_last_revision=project.scm_revision
)
@pytest.fixture @pytest.fixture

View File

@@ -13,9 +13,7 @@ from awx.main.models import InventorySource, Inventory, ActivityStream
@pytest.fixture @pytest.fixture
def scm_inventory(inventory, project): def scm_inventory(inventory, project):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
inventory.inventory_sources.create( inventory.inventory_sources.create(name='foobar', source='scm', source_project=project)
name='foobar', update_on_project_update=True, source='scm', source_project=project, scm_last_revision=project.scm_revision
)
return inventory return inventory
@@ -23,9 +21,7 @@ def scm_inventory(inventory, project):
def factory_scm_inventory(inventory, project): def factory_scm_inventory(inventory, project):
def fn(**kwargs): def fn(**kwargs):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
return inventory.inventory_sources.create( return inventory.inventory_sources.create(source_project=project, overwrite_vars=True, source='scm', **kwargs)
source_project=project, overwrite_vars=True, source='scm', scm_last_revision=project.scm_revision, **kwargs
)
return fn return fn
@@ -544,15 +540,12 @@ class TestControlledBySCM:
def test_safe_method_works(self, get, options, scm_inventory, admin_user): def test_safe_method_works(self, get, options, scm_inventory, admin_user):
get(scm_inventory.get_absolute_url(), admin_user, expect=200) get(scm_inventory.get_absolute_url(), admin_user, expect=200)
options(scm_inventory.get_absolute_url(), admin_user, expect=200) options(scm_inventory.get_absolute_url(), admin_user, expect=200)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision != ''
def test_vars_edit_reset(self, patch, scm_inventory, admin_user): def test_vars_edit_reset(self, patch, scm_inventory, admin_user):
patch(scm_inventory.get_absolute_url(), {'variables': 'hello: world'}, admin_user, expect=200) patch(scm_inventory.get_absolute_url(), {'variables': 'hello: world'}, admin_user, expect=200)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == ''
def test_name_edit_allowed(self, patch, scm_inventory, admin_user): def test_name_edit_allowed(self, patch, scm_inventory, admin_user):
patch(scm_inventory.get_absolute_url(), {'variables': '---', 'name': 'newname'}, admin_user, expect=200) patch(scm_inventory.get_absolute_url(), {'variables': '---', 'name': 'newname'}, admin_user, expect=200)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision != ''
def test_host_associations_reset(self, post, scm_inventory, admin_user): def test_host_associations_reset(self, post, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -560,14 +553,12 @@ class TestControlledBySCM:
g = inv_src.groups.create(name='fooland', inventory=scm_inventory) g = inv_src.groups.create(name='fooland', inventory=scm_inventory)
post(reverse('api:host_groups_list', kwargs={'pk': h.id}), {'id': g.id}, admin_user, expect=204) post(reverse('api:host_groups_list', kwargs={'pk': h.id}), {'id': g.id}, admin_user, expect=204)
post(reverse('api:group_hosts_list', kwargs={'pk': g.id}), {'id': h.id}, admin_user, expect=204) post(reverse('api:group_hosts_list', kwargs={'pk': g.id}), {'id': h.id}, admin_user, expect=204)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == ''
def test_group_group_associations_reset(self, post, scm_inventory, admin_user): def test_group_group_associations_reset(self, post, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
g1 = inv_src.groups.create(name='barland', inventory=scm_inventory) g1 = inv_src.groups.create(name='barland', inventory=scm_inventory)
g2 = inv_src.groups.create(name='fooland', inventory=scm_inventory) g2 = inv_src.groups.create(name='fooland', inventory=scm_inventory)
post(reverse('api:group_children_list', kwargs={'pk': g1.id}), {'id': g2.id}, admin_user, expect=204) post(reverse('api:group_children_list', kwargs={'pk': g1.id}), {'id': g2.id}, admin_user, expect=204)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == ''
def test_host_group_delete_reset(self, delete, scm_inventory, admin_user): def test_host_group_delete_reset(self, delete, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -575,7 +566,6 @@ class TestControlledBySCM:
g = inv_src.groups.create(name='fooland', inventory=scm_inventory) g = inv_src.groups.create(name='fooland', inventory=scm_inventory)
delete(h.get_absolute_url(), admin_user, expect=204) delete(h.get_absolute_url(), admin_user, expect=204)
delete(g.get_absolute_url(), admin_user, expect=204) delete(g.get_absolute_url(), admin_user, expect=204)
assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == ''
def test_remove_scm_inv_src(self, delete, scm_inventory, admin_user): def test_remove_scm_inv_src(self, delete, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -588,7 +578,6 @@ class TestControlledBySCM:
{ {
'name': 'new inv src', 'name': 'new inv src',
'source_project': project.pk, 'source_project': project.pk,
'update_on_project_update': False,
'source': 'scm', 'source': 'scm',
'overwrite_vars': True, 'overwrite_vars': True,
'source_vars': 'plugin: a.b.c', 'source_vars': 'plugin: a.b.c',
@@ -597,27 +586,6 @@ class TestControlledBySCM:
expect=201, expect=201,
) )
def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user):
post(
reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}),
{'name': 'new inv src', 'source_project': project.pk, 'update_on_project_update': True, 'source': 'scm', 'overwrite_vars': True},
admin_user,
expect=400,
)
def test_two_update_on_project_update_inv_src_prohibited(self, patch, scm_inventory, factory_scm_inventory, project, admin_user):
scm_inventory2 = factory_scm_inventory(name="scm_inventory2")
res = patch(
reverse('api:inventory_source_detail', kwargs={'pk': scm_inventory2.id}),
{
'update_on_project_update': True,
},
admin_user,
expect=400,
)
content = json.loads(res.content)
assert content['update_on_project_update'] == ["More than one SCM-based inventory source with update on project update " "per-inventory not allowed."]
def test_adding_inv_src_without_proj_access_prohibited(self, post, project, inventory, rando): def test_adding_inv_src_without_proj_access_prohibited(self, post, project, inventory, rando):
inventory.admin_role.members.add(rando) inventory.admin_role.members.add(rando)
post( post(

View File

@@ -347,9 +347,7 @@ def scm_inventory_source(inventory, project):
source_project=project, source_project=project,
source='scm', source='scm',
source_path='inventory_file', source_path='inventory_file',
update_on_project_update=True,
inventory=inventory, inventory=inventory,
scm_last_revision=project.scm_revision,
) )
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
inv_src.save() inv_src.save()

View File

@@ -3,8 +3,6 @@
import pytest import pytest
from unittest import mock from unittest import mock
from django.core.exceptions import ValidationError
# AWX # AWX
from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job
from awx.main.constants import CLOUD_PROVIDERS from awx.main.constants import CLOUD_PROVIDERS
@@ -123,19 +121,6 @@ class TestActiveCount:
@pytest.mark.django_db @pytest.mark.django_db
class TestSCMUpdateFeatures: class TestSCMUpdateFeatures:
def test_automatic_project_update_on_create(self, inventory, project):
inv_src = InventorySource(source_project=project, source_path='inventory_file', inventory=inventory, update_on_project_update=True, source='scm')
with mock.patch.object(inv_src, 'update') as mck_update:
inv_src.save()
mck_update.assert_called_once_with()
def test_reset_scm_revision(self, scm_inventory_source):
starting_rev = scm_inventory_source.scm_last_revision
assert starting_rev != ''
scm_inventory_source.source_path = '/newfolder/newfile.ini'
scm_inventory_source.save()
assert scm_inventory_source.scm_last_revision == ''
def test_source_location(self, scm_inventory_source): def test_source_location(self, scm_inventory_source):
# Combines project directory with the inventory file specified # Combines project directory with the inventory file specified
inventory_update = InventoryUpdate(inventory_source=scm_inventory_source, source_path=scm_inventory_source.source_path) inventory_update = InventoryUpdate(inventory_source=scm_inventory_source, source_path=scm_inventory_source.source_path)
@@ -167,22 +152,6 @@ class TestRelatedJobs:
assert job.id in [jerb.id for jerb in group._get_related_jobs()] assert job.id in [jerb.id for jerb in group._get_related_jobs()]
@pytest.mark.django_db
class TestSCMClean:
def test_clean_update_on_project_update_multiple(self, inventory):
inv_src1 = InventorySource(inventory=inventory, update_on_project_update=True, source='scm')
inv_src1.clean_update_on_project_update()
inv_src1.save()
inv_src1.source_vars = '---\nhello: world'
inv_src1.clean_update_on_project_update()
inv_src2 = InventorySource(inventory=inventory, update_on_project_update=True, source='scm')
with pytest.raises(ValidationError):
inv_src2.clean_update_on_project_update()
@pytest.mark.django_db @pytest.mark.django_db
class TestInventorySourceInjectors: class TestInventorySourceInjectors:
def test_extra_credentials(self, project, credential): def test_extra_credentials(self, project, credential):

View File

@@ -19,6 +19,7 @@ from awx.api.views import WorkflowJobTemplateNodeSuccessNodesList
# Django # Django
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.timezone import now
class TestWorkflowDAGFunctional(TransactionTestCase): class TestWorkflowDAGFunctional(TransactionTestCase):
@@ -381,3 +382,38 @@ def test_workflow_ancestors_recursion_prevention(organization):
WorkflowJobNode.objects.create(workflow_job=wfj, unified_job_template=wfjt, job=wfj) # well, this is a problem WorkflowJobNode.objects.create(workflow_job=wfj, unified_job_template=wfjt, job=wfj) # well, this is a problem
# mostly, we just care that this assertion finishes in finite time # mostly, we just care that this assertion finishes in finite time
assert wfj.get_ancestor_workflows() == [] assert wfj.get_ancestor_workflows() == []
@pytest.mark.django_db
class TestCombinedArtifacts:
@pytest.fixture
def wfj_artifacts(self, job_template, organization):
wfjt = WorkflowJobTemplate.objects.create(organization=organization, name='has_artifacts')
wfj = WorkflowJob.objects.create(workflow_job_template=wfjt, launch_type='workflow')
job = job_template.create_unified_job(_eager_fields=dict(artifacts={'foooo': 'bar'}, status='successful', finished=now()))
WorkflowJobNode.objects.create(workflow_job=wfj, unified_job_template=job_template, job=job)
return wfj
def test_multiple_types(self, project, wfj_artifacts):
project_update = project.create_unified_job()
WorkflowJobNode.objects.create(workflow_job=wfj_artifacts, unified_job_template=project, job=project_update)
assert wfj_artifacts.get_effective_artifacts() == {'foooo': 'bar'}
def test_precedence_based_on_time(self, wfj_artifacts, job_template):
later_job = job_template.create_unified_job(
_eager_fields=dict(artifacts={'foooo': 'zoo'}, status='successful', finished=now()) # finished later, should win
)
WorkflowJobNode.objects.create(workflow_job=wfj_artifacts, unified_job_template=job_template, job=later_job)
assert wfj_artifacts.get_effective_artifacts() == {'foooo': 'zoo'}
def test_bad_data_with_artifacts(self, organization):
# This is toxic database data, this tests that it doesn't create an infinite loop
wfjt = WorkflowJobTemplate.objects.create(organization=organization, name='child')
wfj = WorkflowJob.objects.create(workflow_job_template=wfjt, launch_type='workflow')
WorkflowJobNode.objects.create(workflow_job=wfj, unified_job_template=wfjt, job=wfj)
job = Job.objects.create(artifacts={'foo': 'bar'}, status='successful')
WorkflowJobNode.objects.create(workflow_job=wfj, job=job)
# mostly, we just care that this assertion finishes in finite time
assert wfj.get_effective_artifacts() == {'foo': 'bar'}

View File

@@ -4,9 +4,8 @@ import os
import tempfile import tempfile
import shutil import shutil
from awx.main.tasks.jobs import RunProjectUpdate, RunInventoryUpdate
from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files
from awx.main.models import ProjectUpdate, InventoryUpdate, InventorySource, Instance, Job from awx.main.models import Instance, Job
@pytest.fixture @pytest.fixture
@@ -27,63 +26,6 @@ def test_no_worker_info_on_AWX_nodes(node_type):
execution_node_health_check(hostname) execution_node_health_check(hostname)
@pytest.mark.django_db
class TestDependentInventoryUpdate:
def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file, mock_me):
task = RunProjectUpdate()
task.revision_path = scm_revision_file
proj_update = scm_inventory_source.source_project.create_project_update()
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
with mock.patch.object(RunProjectUpdate, 'release_lock'):
task.post_run_hook(proj_update, 'successful')
inv_update_mck.assert_called_once_with(proj_update, mock.ANY)
def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file, mock_me):
task = RunProjectUpdate()
task.revision_path = scm_revision_file
proj_update = project.create_project_update()
with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
with mock.patch.object(RunProjectUpdate, 'release_lock'):
task.post_run_hook(proj_update, 'successful')
assert not inv_update_mck.called
def test_dependent_inventory_updates(self, scm_inventory_source, default_instance_group, mock_me):
task = RunProjectUpdate()
scm_inventory_source.scm_last_revision = ''
proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
with mock.patch('awx.main.tasks.jobs.create_partition'):
task._update_dependent_inventories(proj_update, [scm_inventory_source])
assert InventoryUpdate.objects.count() == 1
inv_update = InventoryUpdate.objects.first()
iu_run_mock.assert_called_once_with(inv_update.id)
assert inv_update.source_project_update_id == proj_update.pk
def test_dependent_inventory_project_cancel(self, project, inventory, default_instance_group, mock_me):
"""
Test that dependent inventory updates exhibit good behavior on cancel
of the source project update
"""
task = RunProjectUpdate()
proj_update = ProjectUpdate.objects.create(project=project)
kwargs = dict(source_project=project, source='scm', source_path='inventory_file', update_on_project_update=True, inventory=inventory)
is1 = InventorySource.objects.create(name="test-scm-inv", **kwargs)
is2 = InventorySource.objects.create(name="test-scm-inv2", **kwargs)
def user_cancels_project(pk):
ProjectUpdate.objects.all().update(cancel_flag=True)
with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
with mock.patch('awx.main.tasks.jobs.create_partition'):
iu_run_mock.side_effect = user_cancels_project
task._update_dependent_inventories(proj_update, [is1, is2])
# Verify that it bails after 1st update, detecting a cancel
assert is2.inventory_updates.count() == 0
iu_run_mock.assert_called_once()
@pytest.fixture @pytest.fixture
def mock_job_folder(request): def mock_job_folder(request):
pdd_path = tempfile.mkdtemp(prefix='awx_123_') pdd_path = tempfile.mkdtemp(prefix='awx_123_')

View File

@@ -69,21 +69,21 @@ class TestJobTemplateLabelList:
class TestInventoryInventorySourcesUpdate: class TestInventoryInventorySourcesUpdate:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"can_update, can_access, is_source, is_up_on_proj, expected", "can_update, can_access, is_source, expected",
[ [
(True, True, "ec2", False, [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), (True, True, "ec2", [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]),
(False, True, "gce", False, [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]), (False, True, "gce", [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]),
(True, False, "scm", True, [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), (True, False, "scm", [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]),
], ],
) )
def test_post(self, mocker, can_update, can_access, is_source, is_up_on_proj, expected): def test_post(self, mocker, can_update, can_access, is_source, expected):
class InventoryUpdate: class InventoryUpdate:
id = 1 id = 1
class Project: class Project:
name = 'project' name = 'project'
InventorySource = namedtuple('InventorySource', ['source', 'update_on_project_update', 'pk', 'can_update', 'update', 'source_project']) InventorySource = namedtuple('InventorySource', ['source', 'pk', 'can_update', 'update', 'source_project'])
class InventorySources(object): class InventorySources(object):
def all(self): def all(self):
@@ -92,7 +92,6 @@ class TestInventoryInventorySourcesUpdate:
pk=1, pk=1,
source=is_source, source=is_source,
source_project=Project, source_project=Project,
update_on_project_update=is_up_on_proj,
can_update=can_update, can_update=can_update,
update=lambda: InventoryUpdate, update=lambda: InventoryUpdate,
) )

View File

@@ -1,28 +1,13 @@
import pytest import pytest
from unittest import mock
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from awx.main.models import ( from awx.main.models import (
UnifiedJob,
InventoryUpdate, InventoryUpdate,
InventorySource, InventorySource,
) )
def test_cancel(mocker):
with mock.patch.object(UnifiedJob, 'cancel', return_value=True) as parent_cancel:
iu = InventoryUpdate()
iu.save = mocker.MagicMock()
build_job_explanation_mock = mocker.MagicMock()
iu._build_job_explanation = mocker.MagicMock(return_value=build_job_explanation_mock)
iu.cancel()
parent_cancel.assert_called_with(is_chain=False, job_explanation=None)
def test__build_job_explanation(): def test__build_job_explanation():
iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update') iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update')
@@ -53,9 +38,3 @@ class TestControlledBySCM:
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
inv_src.clean_source_path() inv_src.clean_source_path()
def test_clean_update_on_launch_update_on_project_update(self):
inv_src = InventorySource(update_on_project_update=True, update_on_launch=True, source='scm')
with pytest.raises(ValidationError):
inv_src.clean_update_on_launch()

View File

@@ -1,7 +1,7 @@
from awx.main.tasks.callback import RunnerCallback from awx.main.tasks.callback import RunnerCallback
from awx.main.constants import ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE from awx.main.constants import ANSIBLE_RUNNER_NEEDS_UPDATE_MESSAGE
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
def test_delay_update(mock_me): def test_delay_update(mock_me):

View File

@@ -0,0 +1,50 @@
import signal
from awx.main.tasks.signals import signal_state, signal_callback, with_signal_handling
def test_outer_inner_signal_handling():
"""
Even if the flag is set in the outer context, its value should persist in the inner context
"""
@with_signal_handling
def f2():
assert signal_callback()
@with_signal_handling
def f1():
assert signal_callback() is False
signal_state.set_flag()
assert signal_callback()
f2()
original_sigterm = signal.getsignal(signal.SIGTERM)
assert signal_callback() is False
f1()
assert signal_callback() is False
assert signal.getsignal(signal.SIGTERM) is original_sigterm
def test_inner_outer_signal_handling():
"""
Even if the flag is set in the inner context, its value should persist in the outer context
"""
@with_signal_handling
def f2():
assert signal_callback() is False
signal_state.set_flag()
assert signal_callback()
@with_signal_handling
def f1():
assert signal_callback() is False
f2()
assert signal_callback()
original_sigterm = signal.getsignal(signal.SIGTERM)
assert signal_callback() is False
f1()
assert signal_callback() is False
assert signal.getsignal(signal.SIGTERM) is original_sigterm

View File

@@ -922,7 +922,8 @@ class TestJobCredentials(TestJobExecution):
assert env['AWS_SECURITY_TOKEN'] == 'token' assert env['AWS_SECURITY_TOKEN'] == 'token'
assert safe_env['AWS_SECRET_ACCESS_KEY'] == HIDDEN_PASSWORD assert safe_env['AWS_SECRET_ACCESS_KEY'] == HIDDEN_PASSWORD
def test_gce_credentials(self, private_data_dir, job, mock_me): @pytest.mark.parametrize("cred_env_var", ['GCE_CREDENTIALS_FILE_PATH', 'GOOGLE_APPLICATION_CREDENTIALS'])
def test_gce_credentials(self, cred_env_var, private_data_dir, job, mock_me):
gce = CredentialType.defaults['gce']() gce = CredentialType.defaults['gce']()
credential = Credential(pk=1, credential_type=gce, inputs={'username': 'bob', 'project': 'some-project', 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY}) credential = Credential(pk=1, credential_type=gce, inputs={'username': 'bob', 'project': 'some-project', 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY})
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data') credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
@@ -931,7 +932,7 @@ class TestJobCredentials(TestJobExecution):
env = {} env = {}
safe_env = {} safe_env = {}
credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir) credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir)
runner_path = env['GCE_CREDENTIALS_FILE_PATH'] runner_path = env[cred_env_var]
local_path = to_host_path(runner_path, private_data_dir) local_path = to_host_path(runner_path, private_data_dir)
json_data = json.load(open(local_path, 'rb')) json_data = json.load(open(local_path, 'rb'))
assert json_data['type'] == 'service_account' assert json_data['type'] == 'service_account'
@@ -1316,6 +1317,7 @@ class TestJobCredentials(TestJobExecution):
assert env['AZURE_AD_USER'] == 'bob' assert env['AZURE_AD_USER'] == 'bob'
assert env['AZURE_PASSWORD'] == 'secret' assert env['AZURE_PASSWORD'] == 'secret'
# Because this is testing a mix of multiple cloud creds, we are not going to test the GOOGLE_APPLICATION_CREDENTIALS here
path = to_host_path(env['GCE_CREDENTIALS_FILE_PATH'], private_data_dir) path = to_host_path(env['GCE_CREDENTIALS_FILE_PATH'], private_data_dir)
json_data = json.load(open(path, 'rb')) json_data = json.load(open(path, 'rb'))
assert json_data['type'] == 'service_account' assert json_data['type'] == 'service_account'
@@ -1645,7 +1647,8 @@ class TestInventoryUpdateCredentials(TestJobExecution):
assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD assert safe_env['AZURE_PASSWORD'] == HIDDEN_PASSWORD
def test_gce_source(self, inventory_update, private_data_dir, mocker, mock_me): @pytest.mark.parametrize("cred_env_var", ['GCE_CREDENTIALS_FILE_PATH', 'GOOGLE_APPLICATION_CREDENTIALS'])
def test_gce_source(self, cred_env_var, inventory_update, private_data_dir, mocker, mock_me):
task = jobs.RunInventoryUpdate() task = jobs.RunInventoryUpdate()
task.instance = inventory_update task.instance = inventory_update
gce = CredentialType.defaults['gce']() gce = CredentialType.defaults['gce']()
@@ -1669,7 +1672,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir) credential.credential_type.inject_credential(credential, env, safe_env, [], private_data_dir)
assert env['GCE_ZONE'] == expected_gce_zone assert env['GCE_ZONE'] == expected_gce_zone
json_data = json.load(open(env['GCE_CREDENTIALS_FILE_PATH'], 'rb')) json_data = json.load(open(env[cred_env_var], 'rb'))
assert json_data['type'] == 'service_account' assert json_data['type'] == 'service_account'
assert json_data['private_key'] == self.EXAMPLE_PRIVATE_KEY assert json_data['private_key'] == self.EXAMPLE_PRIVATE_KEY
assert json_data['client_email'] == 'bob' assert json_data['client_email'] == 'bob'

View File

@@ -3,6 +3,8 @@ from django.db import transaction, DatabaseError, InterfaceError
import logging import logging
import time import time
from awx.main.tasks.signals import signal_callback
logger = logging.getLogger('awx.main.tasks.utils') logger = logging.getLogger('awx.main.tasks.utils')
@@ -37,7 +39,10 @@ def update_model(model, pk, _attempt=0, _max_attempts=5, select_for_update=False
# Attempt to retry the update, assuming we haven't already # Attempt to retry the update, assuming we haven't already
# tried too many times. # tried too many times.
if _attempt < _max_attempts: if _attempt < _max_attempts:
time.sleep(5) for i in range(5):
time.sleep(1)
if signal_callback():
raise RuntimeError(f'Could not fetch {pk} because of receiving abort signal')
return update_model(model, pk, _attempt=_attempt + 1, _max_attempts=_max_attempts, **updates) return update_model(model, pk, _attempt=_attempt + 1, _max_attempts=_max_attempts, **updates)
else: else:
logger.error('Failed to update %s after %d retries.', model._meta.object_name, _attempt) logger.error('Failed to update %s after %d retries.', model._meta.object_name, _attempt)

View File

@@ -13,6 +13,7 @@ This includes lists that aren't necessarily hyperlinked, like lookup lists. The
In current smart search typing a term with no key utilizes `?search=` i.e. for "foo" tag, `?search=foo` is given. `?search=` looks on a static list of field name "guesses" (such as name, description, etc.), as well as specific fields as defined for each endpoint (for example, the events endpoint looks for a "stdout" field as well). Due to the fact a key will always be present on the left-hand of simple search, it doesn't make sense to use `?search=` as the default. In current smart search typing a term with no key utilizes `?search=` i.e. for "foo" tag, `?search=foo` is given. `?search=` looks on a static list of field name "guesses" (such as name, description, etc.), as well as specific fields as defined for each endpoint (for example, the events endpoint looks for a "stdout" field as well). Due to the fact a key will always be present on the left-hand of simple search, it doesn't make sense to use `?search=` as the default.
We may allow passing of `?search=` through our future advanced search interface. Some details that were gathered in planning phases about `?search=` that might be helpful in the future: We may allow passing of `?search=` through our future advanced search interface. Some details that were gathered in planning phases about `?search=` that might be helpful in the future:
- `?search=` tags are OR'd together (union is returned). - `?search=` tags are OR'd together (union is returned).
- `?search=foo&name=bar` returns items that have a name field of bar (not case insensitive) AND some text field with foo on it - `?search=foo&name=bar` returns items that have a name field of bar (not case insensitive) AND some text field with foo on it
- `?search=foo&search=bar&name=baz` returns (foo in name OR foo in description OR ...) AND (bar in name OR bar in description OR ...) AND (baz in name) - `?search=foo&search=bar&name=baz` returns (foo in name OR foo in description OR ...) AND (bar in name OR bar in description OR ...) AND (baz in name)
@@ -50,6 +51,7 @@ This was brought up as a nice to have when we were discussing our initial implem
- DONE remove button for search tags of duplicate keys are broken, fix that - DONE remove button for search tags of duplicate keys are broken, fix that
### TODO pre-holiday break ### TODO pre-holiday break
- Update COLUMNS to SORT_COLUMNS and SEARCH_COLUMNS - Update COLUMNS to SORT_COLUMNS and SEARCH_COLUMNS
- Update to using new PF Toolbar component (currently an experimental component) - Update to using new PF Toolbar component (currently an experimental component)
- Change the right-hand input based on the type of key selected on the left-hand side. In addition to text input, for our MVP we will support: - Change the right-hand input based on the type of key selected on the left-hand side. In addition to text input, for our MVP we will support:
@@ -58,163 +60,188 @@ This was brought up as a nice to have when we were discussing our initial implem
- Update the following lists to have the following keys: - Update the following lists to have the following keys:
**Jobs list** (signed off earlier in chat) **Jobs list** (signed off earlier in chat)
- Name (which is also the name of the job template) - search is ?name=jt
- Job ID - search is ?id=13 - Name (which is also the name of the job template) - search is ?name=jt
- Label name - search is ?labels__name=foo - Job ID - search is ?id=13
- Job type (dropdown on right with the different types) ?type = job - Label name - search is ?labels\_\_name=foo
- Created by (username) - search is ?created_by__username=admin - Job type (dropdown on right with the different types) ?type = job
- Status - search (dropdown on right with different statuses) is ?status=successful - Created by (username) - search is ?created_by\_\_username=admin
- Status - search (dropdown on right with different statuses) is ?status=successful
Instances of jobs list include: Instances of jobs list include:
- Jobs list
- Host completed jobs list - Jobs list
- JT completed jobs list - Host completed jobs list
- JT completed jobs list
**Organization list** **Organization list**
- Name - search is ?name=org
- ? Team name (of a team in the org) - search is ?teams__name=ansible - Name - search is ?name=org
- ? Username (of a user in the org) - search is ?users__username=johndoe - ? Team name (of a team in the org) - search is ?teams\_\_name=ansible
- ? Username (of a user in the org) - search is ?users\_\_username=johndoe
Instances of orgs list include: Instances of orgs list include:
- Orgs list
- User orgs list - Orgs list
- Lookup on Project - User orgs list
- Lookup on Credential - Lookup on Project
- Lookup on Inventory - Lookup on Credential
- User access add wizard list - Lookup on Inventory
- Team access add wizard list - User access add wizard list
- Team access add wizard list
**Instance Groups list** **Instance Groups list**
- Name - search is ?name=ig
- ? is_container_group boolean choice (doesn't work right now in API but will soon) - search is ?is_container_group=true - Name - search is ?name=ig
- ? credential name - search is ?credentials__name=kubey - ? is_container_group boolean choice (doesn't work right now in API but will soon) - search is ?is_container_group=true
- ? credential name - search is ?credentials\_\_name=kubey
Instance of instance groups list include: Instance of instance groups list include:
- Lookup on Org
- Lookup on JT - Lookup on Org
- Lookup on Inventory - Lookup on JT
- Lookup on Inventory
**Users list** **Users list**
- Username - search is ?username=johndoe
- First Name - search is ?first_name=John - Username - search is ?username=johndoe
- Last Name - search is ?last_name=Doe - First Name - search is ?first_name=John
- ? (if not superfluous, would not include on Team users list) Team Name - search is ?teams__name=team_of_john_does (note API issue: User has no field named "teams") - Last Name - search is ?last_name=Doe
- ? (only for access or permissions list) Role Name - search is ?roles__name=Admin (note API issue: Role has no field "name") - ? (if not superfluous, would not include on Team users list) Team Name - search is ?teams\_\_name=team_of_john_does (note API issue: User has no field named "teams")
- ? (if not superfluous, would not include on Organization users list) ORg Name - search is ?organizations__name=org_of_jhn_does - ? (only for access or permissions list) Role Name - search is ?roles\_\_name=Admin (note API issue: Role has no field "name")
- ? (if not superfluous, would not include on Organization users list) ORg Name - search is ?organizations\_\_name=org_of_jhn_does
Instance of user lists include: Instance of user lists include:
- User list
- Org user list - User list
- Access list for Org, JT, Project, Credential, Inventory, User and Team - Org user list
- Access list for JT - Access list for Org, JT, Project, Credential, Inventory, User and Team
- Access list Project - Access list for JT
- Access list for Credential - Access list Project
- Access list for Inventory - Access list for Credential
- Access list for User - Access list for Inventory
- Access list for Team - Access list for User
- Team add users list - Access list for Team
- Users list in access wizard (to add new roles for a particular list) for Org - Team add users list
- Users list in access wizard (to add new roles for a particular list) for JT - Users list in access wizard (to add new roles for a particular list) for Org
- Users list in access wizard (to add new roles for a particular list) for Project - Users list in access wizard (to add new roles for a particular list) for JT
- Users list in access wizard (to add new roles for a particular list) for Credential - Users list in access wizard (to add new roles for a particular list) for Project
- Users list in access wizard (to add new roles for a particular list) for Inventory - Users list in access wizard (to add new roles for a particular list) for Credential
- Users list in access wizard (to add new roles for a particular list) for Inventory
**Teams list** **Teams list**
- Name - search is ?name=teamname
- ? Username (of a user in the team) - search is ?users__username=johndoe - Name - search is ?name=teamname
- ? (if not superfluous, would not include on Organizations teams list) Org Name - search is ?organizations__name=org_of_john_does - ? Username (of a user in the team) - search is ?users\_\_username=johndoe
- ? (if not superfluous, would not include on Organizations teams list) Org Name - search is ?organizations\_\_name=org_of_john_does
Instance of team lists include: Instance of team lists include:
- Team list
- Org team list - Team list
- User team list - Org team list
- Team list in access wizard (to add new roles for a particular list) for Org - User team list
- Team list in access wizard (to add new roles for a particular list) for JT - Team list in access wizard (to add new roles for a particular list) for Org
- Team list in access wizard (to add new roles for a particular list) for Project - Team list in access wizard (to add new roles for a particular list) for JT
- Team list in access wizard (to add new roles for a particular list) for Credential - Team list in access wizard (to add new roles for a particular list) for Project
- Team list in access wizard (to add new roles for a particular list) for Inventory - Team list in access wizard (to add new roles for a particular list) for Credential
- Team list in access wizard (to add new roles for a particular list) for Inventory
**Credentials list** **Credentials list**
- Name
- ? Type (dropdown on right with different types) - Name
- ? Created by (username) - ? Type (dropdown on right with different types)
- ? Modified by (username) - ? Created by (username)
- ? Modified by (username)
Instance of credential lists include: Instance of credential lists include:
- Credential list
- Lookup for JT - Credential list
- Lookup for Project - Lookup for JT
- User access add wizard list - Lookup for Project
- Team access add wizard list - User access add wizard list
- Team access add wizard list
**Projects list** **Projects list**
- Name - search is ?name=proj
- ? Type (dropdown on right with different types) - search is scm_type=git - Name - search is ?name=proj
- ? SCM URL - search is ?scm_url=github.com/ansible/test-playbooks - ? Type (dropdown on right with different types) - search is scm_type=git
- ? Created by (username) - search is ?created_by__username=admin - ? SCM URL - search is ?scm_url=github.com/ansible/test-playbooks
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of project lists include: Instance of project lists include:
- Project list
- Lookup for JT - Project list
- User access add wizard list - Lookup for JT
- Team access add wizard list - User access add wizard list
- Team access add wizard list
**Templates list** **Templates list**
- Name - search is ?name=cleanup
- ? Type (dropdown on right with different types) - search is ?type=playbook_run - Name - search is ?name=cleanup
- ? Playbook name - search is ?job_template__playbook=debug.yml - ? Type (dropdown on right with different types) - search is ?type=playbook_run
- ? Created by (username) - search is ?created_by__username=admin - ? Playbook name - search is ?job_template\_\_playbook=debug.yml
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of template lists include: Instance of template lists include:
- Template list
- Project Templates list - Template list
- Project Templates list
**Inventories list** **Inventories list**
- Name - search is ?name=inv
- ? Created by (username) - search is ?created_by__username=admin - Name - search is ?name=inv
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of inventory lists include: Instance of inventory lists include:
- Inventory list
- Lookup for JT - Inventory list
- User access add wizard list - Lookup for JT
- Team access add wizard list - User access add wizard list
- Team access add wizard list
**Groups list** **Groups list**
- Name - search is ?name=group_name
- ? Created by (username) - search is ?created_by__username=admin - Name - search is ?name=group_name
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of group lists include: Instance of group lists include:
- Group list
- Group list
**Hosts list** **Hosts list**
- Name - search is ?name=hostname
- ? Created by (username) - search is ?created_by__username=admin - Name - search is ?name=hostname
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of host lists include: Instance of host lists include:
- Host list
- Host list
**Notifications list** **Notifications list**
- Name - search is ?name=notification_template_name
- ? Type (dropdown on right with different types) - search is ?type=slack - Name - search is ?name=notification_template_name
- ? Created by (username) - search is ?created_by__username=admin - ? Type (dropdown on right with different types) - search is ?type=slack
- ? Modified by (username) - search is ?modified_by__username=admin - ? Created by (username) - search is ?created_by\_\_username=admin
- ? Modified by (username) - search is ?modified_by\_\_username=admin
Instance of notification lists include: Instance of notification lists include:
- Org notification list
- JT notification list - Org notification list
- Project notification list - JT notification list
- Project notification list
### TODO backlog ### TODO backlog
- Change the right-hand input based on the type of key selected on the left-hand side. We will eventually want to support: - Change the right-hand input based on the type of key selected on the left-hand side. We will eventually want to support:
- lookup input (selection of particular resources, based on API list endpoints) - lookup input (selection of particular resources, based on API list endpoints)
- date picker input - date picker input
- Update the following lists to have the following keys: - Update the following lists to have the following keys:
- Update all __name and __username related field search-based keys to be type-ahead lookup based searches - Update all **name and **username related field search-based keys to be type-ahead lookup based searches
## Code Details ## Code Details
@@ -262,6 +289,7 @@ Similar to the way the list grabs data based on changes to the react-router para
### qs utility ### qs utility
The qs (queryset) utility is used to make the search speak the language of the REST API. The main functions of the utilities are to: The qs (queryset) utility is used to make the search speak the language of the REST API. The main functions of the utilities are to:
- add, replace and remove filters - add, replace and remove filters
- translate filters as url params (for linking and maintaining state), in-memory representation (as JS objects), and params that Django REST Framework understands. - translate filters as url params (for linking and maintaining state), in-memory representation (as JS objects), and params that Django REST Framework understands.
@@ -317,33 +345,33 @@ We will try to form options data from a static file. Because options data is st
Smart search will be able to craft the tag through various states. Note that the phases don't necessarily need to be completed in sequential order. Smart search will be able to craft the tag through various states. Note that the phases don't necessarily need to be completed in sequential order.
PHASE 1: prefix operators PHASE 1: prefix operators
**TODO: Double check there's no reason we need to include or__ and chain__ and can just do not__** **TODO: Double check there's no reason we need to include or** and chain** and can just do not\_\_**
- not__ - not\_\_
- or__ - or\_\_
- chain__ - chain\_\_
how these work: how these work:
To exclude results matching certain criteria, prefix the field parameter with not__: To exclude results matching certain criteria, prefix the field parameter with not\_\_:
?not__field=value ?not**field=value
By default, all query string filters are AND'ed together, so only the results matching all filters will be returned. To combine results matching any one of multiple criteria, prefix each query string parameter with or__: By default, all query string filters are AND'ed together, so only the results matching all filters will be returned. To combine results matching any one of multiple criteria, prefix each query string parameter with or**:
?or__field=value&or__field=othervalue ?or**field=value&or**field=othervalue
?or__not__field=value&or__field=othervalue ?or**not**field=value&or**field=othervalue
(Added in Ansible Tower 1.4.5) The default AND filtering applies all filters simultaneously to each related object being filtered across database relationships. The chain filter instead applies filters separately for each related object. To use, prefix the query string parameter with chain__: (Added in Ansible Controller 1.4.5) The default AND filtering applies all filters simultaneously to each related object being filtered across database relationships. The chain filter instead applies filters separately for each related object. To use, prefix the query string parameter with chain**:
?chain__related__field=value&chain__related__field2=othervalue ?chain**related**field=value&chain**related**field2=othervalue
?chain__not__related__field=value&chain__related__field2=othervalue ?chain**not**related**field=value&chain**related**field2=othervalue
If the first query above were written as ?related__field=value&related__field2=othervalue, it would return only the primary objects where the same related object satisfied both conditions. As written using the chain filter, it would return the intersection of primary objects matching each condition. If the first query above were written as ?related**field=value&related\_\_field2=othervalue, it would return only the primary objects where the same related object satisfied both conditions. As written using the chain filter, it would return the intersection of primary objects matching each condition.
PHASE 2: related fields, given by array, where __search is appended to them, i.e. PHASE 2: related fields, given by array, where \_\_search is appended to them, i.e.
``` ```
"related_search_fields": [ "related_search_fields": [
"credentials__search", "credentials__search",
"labels__search", "labels__search",
"created_by__search", "created_by__search",
@@ -360,30 +388,29 @@ Smart search will be able to craft the tag through various states. Note that th
"workflows__search", "workflows__search",
"instance_groups__search" "instance_groups__search"
], ],
``` ```
PHASE 3: keys, give by object key names for data.actions.GET PHASE 3: keys, give by object key names for data.actions.GET - type is given for each key which we could use to help craft the value
- type is given for each key which we could use to help craft the value
PHASE 4: after key postfix operators can be PHASE 4: after key postfix operators can be
**TODO: will need to figure out which ones we support** **TODO: will need to figure out which ones we support**
- exact: Exact match (default lookup if not specified). - exact: Exact match (default lookup if not specified).
- iexact: Case-insensitive version of exact. - iexact: Case-insensitive version of exact.
- contains: Field contains value. - contains: Field contains value.
- icontains: Case-insensitive version of contains. - icontains: Case-insensitive version of contains.
- startswith: Field starts with value. - startswith: Field starts with value.
- istartswith: Case-insensitive version of startswith. - istartswith: Case-insensitive version of startswith.
- endswith: Field ends with value. - endswith: Field ends with value.
- iendswith: Case-insensitive version of endswith. - iendswith: Case-insensitive version of endswith.
- regex: Field matches the given regular expression. - regex: Field matches the given regular expression.
- iregex: Case-insensitive version of regex. - iregex: Case-insensitive version of regex.
- gt: Greater than comparison. - gt: Greater than comparison.
- gte: Greater than or equal to comparison. - gte: Greater than or equal to comparison.
- lt: Less than comparison. - lt: Less than comparison.
- lte: Less than or equal to comparison. - lte: Less than or equal to comparison.
- isnull: Check whether the given field or related object is null; expects a boolean value. - isnull: Check whether the given field or related object is null; expects a boolean value.
- in: Check whether the given field's value is present in the list provided; expects a list of items. - in: Check whether the given field's value is present in the list provided; expects a list of items.
PHASE 5: The value. Based on options, we can give hints or validation based on type of value (like number fields don't accept "foo" or whatever) PHASE 5: The value. Based on options, we can give hints or validation based on type of value (like number fields don't accept "foo" or whatever)

589
awx/ui/package-lock.json generated
View File

@@ -6,15 +6,15 @@
"": { "": {
"name": "ui", "name": "ui",
"dependencies": { "dependencies": {
"@lingui/react": "3.13.3", "@lingui/react": "3.14.0",
"@patternfly/patternfly": "4.196.7", "@patternfly/patternfly": "4.202.1",
"@patternfly/react-core": "^4.201.0", "@patternfly/react-core": "^4.221.3",
"@patternfly/react-icons": "4.49.19", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-table": "4.83.1", "@patternfly/react-table": "4.93.1",
"ace-builds": "^1.6.0", "ace-builds": "^1.6.0",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"axios": "0.22.0", "axios": "0.27.2",
"codemirror": "^5.65.4", "codemirror": "^6.0.1",
"d3": "7.4.4", "d3": "7.4.4",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"dompurify": "2.3.8", "dompurify": "2.3.8",
@@ -28,7 +28,7 @@
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.3.3",
"react-virtualized": "^9.21.1", "react-virtualized": "^9.21.1",
"rrule": "2.7.0", "rrule": "2.7.0",
"styled-components": "5.3.5" "styled-components": "5.3.5"
@@ -1926,6 +1926,82 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "dev": true
}, },
"node_modules/@codemirror/autocomplete": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.0.2.tgz",
"integrity": "sha512-9PDjnllmXan/7Uax87KGORbxerDJ/cu10SB+n4Jz0zXMEvIh3+TGgZxhIvDOtaQ4jDBQEM7kHYW4vLdQB0DGZQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
},
"peerDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz",
"integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.0.tgz",
"integrity": "sha512-tabB0Ef/BflwoEmTB4a//WZ9P90UQyne9qWB9YFsmeS4bnEqSys7UpGk/da1URMXhyfuzWCwp+AQNMhvu8SfnA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz",
"integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
"integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz",
"integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg=="
},
"node_modules/@codemirror/view": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
"integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@csstools/normalize.css": { "node_modules/@csstools/normalize.css": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz",
@@ -3322,6 +3398,27 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@lezer/common": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
"integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA=="
},
"node_modules/@lezer/highlight": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
"integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.1.0.tgz",
"integrity": "sha512-Iad04uVwk1PvSnj25mqj7zEEIRAsasbsTRmVzI0AUTs/+1Dz1//iYAaoLr7A+Xa7bZDfql5MKTxZmSlkYZD3Dg==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lingui/babel-plugin-extract-messages": { "node_modules/@lingui/babel-plugin-extract-messages": {
"version": "3.8.10", "version": "3.8.10",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.10.tgz", "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.10.tgz",
@@ -3550,9 +3647,9 @@
} }
}, },
"node_modules/@lingui/core": { "node_modules/@lingui/core": {
"version": "3.13.3", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.13.3.tgz", "resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.14.0.tgz",
"integrity": "sha512-3rQDIC7PtPfUuZCSNfU0nziWNMlGk3JhpxENzGrlt1M8w5RHson89Mk1Ce/how+hWzFpumCQDWLDDhyRPpydbg==", "integrity": "sha512-ertREq9oi9B/umxpd/pInm9uFO8FLK2/0FXfDmMqvH5ydswWn/c9nY5YO4W1h4/8LWO45mewypOIyjoue4De1w==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"make-plural": "^6.2.2", "make-plural": "^6.2.2",
@@ -3593,12 +3690,12 @@
} }
}, },
"node_modules/@lingui/react": { "node_modules/@lingui/react": {
"version": "3.13.3", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/@lingui/react/-/react-3.13.3.tgz", "resolved": "https://registry.npmjs.org/@lingui/react/-/react-3.14.0.tgz",
"integrity": "sha512-sCCI5xMcUY9b6w2lwbwy6iHpo1Fb9TDcjcHAD2KI5JueLH+WWQG66tIHiVAlSsQ+hmQ9Tt+f86H05JQEiDdIvg==", "integrity": "sha512-ow9Mtru7f0T2S9AwnPWRejppcucCW0LmoDR3P4wqHjL+eH5f8a6nxd2doxGieC91/2i4qqW88y4K/zXJxwRSQw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"@lingui/core": "^3.13.3" "@lingui/core": "^3.14.0"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@@ -3649,19 +3746,19 @@
"dev": true "dev": true
}, },
"node_modules/@patternfly/patternfly": { "node_modules/@patternfly/patternfly": {
"version": "4.196.7", "version": "4.202.1",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.196.7.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.202.1.tgz",
"integrity": "sha512-hA7Oww411e1p0/IXjC1I+4/1NNis9V+NVBxfUIpRwyuLbCIDHBdtMu2qAPLdKxXjuibV9EE6ZdlT7ra/kcFuJQ==" "integrity": "sha512-cQiiPqmwJOm9onuTfLPQNRlpAZwDIJ/zVfDQeaFqMQyPJtxtKn3lkphz5xErY5dPs9rR4X94ytQ1I9pkVzaPJQ=="
}, },
"node_modules/@patternfly/react-core": { "node_modules/@patternfly/react-core": {
"version": "4.214.1", "version": "4.224.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.214.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.224.1.tgz",
"integrity": "sha512-XHEqXpnBEDyLVdAEDOYlGqFHnN43eNLSD5HABB99xO6541JV9MRnbxs0+v9iYnfhcKh/8bhA9ITXnUi3f2PEvg==", "integrity": "sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ==",
"dependencies": { "dependencies": {
"@patternfly/react-icons": "^4.65.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.64.1", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.66.1", "@patternfly/react-tokens": "^4.76.1",
"focus-trap": "6.2.2", "focus-trap": "6.9.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
"tslib": "^2.0.0" "tslib": "^2.0.0"
@@ -3671,43 +3768,34 @@
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-core/node_modules/@patternfly/react-icons": {
"version": "4.65.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.65.1.tgz",
"integrity": "sha512-CUYFRPztFkR7qrXq/0UAhLjeHd8FdjLe4jBjj8tfKc7OXwxDeZczqNFyRMATZpPaduTH7BU2r3OUjQrgAbquWg==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@patternfly/react-core/node_modules/tslib": { "node_modules/@patternfly/react-core/node_modules/tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}, },
"node_modules/@patternfly/react-icons": { "node_modules/@patternfly/react-icons": {
"version": "4.49.19", "version": "4.75.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.49.19.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.75.1.tgz",
"integrity": "sha512-Pr6JDDKWOnWChkifXKWglKEPo3Q+1CgiUTUrvk4ZbnD7mhq5e/TFxxInB9CPzi278bvnc2YlPyTjpaAcCN0yGw==", "integrity": "sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w==",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0", "react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-styles": { "node_modules/@patternfly/react-styles": {
"version": "4.64.1", "version": "4.74.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.64.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.74.1.tgz",
"integrity": "sha512-+GxULkP2o5Vpr9w+J4NiGOGzhTfNniYzdPGEF/yC+oDoAXB6Q1HJyQnEj+kJH31xNvwmw3G3VFtwRLX4ZWr0oA==" "integrity": "sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A=="
}, },
"node_modules/@patternfly/react-table": { "node_modules/@patternfly/react-table": {
"version": "4.83.1", "version": "4.93.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.83.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.93.1.tgz",
"integrity": "sha512-mkq13x9funh+Nh2Uzj2ZQBOacNYc+a60yUAHZMXgNcljCJ3LTQUoYy6EonvYrqwSrpC7vj8nLt8+/XbDNc0Aig==", "integrity": "sha512-N/zHkNsY3X3yUXPg6COwdZKAFmTCbWm25qCY2aHjrXlIlE2OKWaYvVag0CcTwPiQhIuCumztr9Y2Uw9uvv0Fsw==",
"dependencies": { "dependencies": {
"@patternfly/react-core": "^4.214.1", "@patternfly/react-core": "^4.224.1",
"@patternfly/react-icons": "^4.65.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.64.1", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.66.1", "@patternfly/react-tokens": "^4.76.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
@@ -3716,24 +3804,15 @@
"react-dom": "^16.8.0 || ^17.0.0" "react-dom": "^16.8.0 || ^17.0.0"
} }
}, },
"node_modules/@patternfly/react-table/node_modules/@patternfly/react-icons": {
"version": "4.65.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.65.1.tgz",
"integrity": "sha512-CUYFRPztFkR7qrXq/0UAhLjeHd8FdjLe4jBjj8tfKc7OXwxDeZczqNFyRMATZpPaduTH7BU2r3OUjQrgAbquWg==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@patternfly/react-table/node_modules/tslib": { "node_modules/@patternfly/react-table/node_modules/tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}, },
"node_modules/@patternfly/react-tokens": { "node_modules/@patternfly/react-tokens": {
"version": "4.66.1", "version": "4.76.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.66.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz",
"integrity": "sha512-k0IWqpufM6ezT+3gWlEamqQ7LW9yi8e8cBBlude5IU8eIEqIG6AccwR1WNBEK1wCVWGwVxakLMdf0XBLl4k52Q==" "integrity": "sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw=="
}, },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": { "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.4", "version": "0.5.4",
@@ -5553,8 +5632,7 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
"dev": true
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@@ -5625,11 +5703,25 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "0.22.0", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.22.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-Z0U3uhqQeg1oNcihswf4ZD57O3NrR1+ZXhxaROaWpDmsDTx7T2HNBV2ulBtie2hwJptu8UvgnJoK+BIqdzh/1w==", "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.14.4" "follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
} }
}, },
"node_modules/axobject-query": { "node_modules/axobject-query": {
@@ -6603,9 +6695,18 @@
} }
}, },
"node_modules/codemirror": { "node_modules/codemirror": {
"version": "5.65.4", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.4.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-tytrSm5Rh52b6j36cbDXN+FHwHCl9aroY4BrDZB2NFFL3Wjfq9nuYVLFFhaOYOczKAg3JXTr8BuT8LcE5QY4Iw==" "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
}, },
"node_modules/collect-v8-coverage": { "node_modules/collect-v8-coverage": {
"version": "1.0.1", "version": "1.0.1",
@@ -6651,7 +6752,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -6881,6 +6981,11 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true "dev": true
}, },
"node_modules/crelt": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7915,7 +8020,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -9931,17 +10035,17 @@
"dev": true "dev": true
}, },
"node_modules/focus-trap": { "node_modules/focus-trap": {
"version": "6.2.2", "version": "6.9.2",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.2.2.tgz", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
"integrity": "sha512-qWovH9+LGoKqREvJaTCzJyO0hphQYGz+ap5Hc4NqXHNhZBdxCi5uBPPcaOUw66fHmzXLVwvETLvFgpwPILqKpg==", "integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
"dependencies": { "dependencies": {
"tabbable": "^5.1.4" "tabbable": "^5.3.2"
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.8", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -11378,7 +11482,7 @@
"node_modules/isarray": { "node_modules/isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
}, },
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
@@ -15501,7 +15605,6 @@
"version": "1.51.0", "version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -15510,7 +15613,6 @@
"version": "2.1.34", "version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dev": true,
"dependencies": { "dependencies": {
"mime-db": "1.51.0" "mime-db": "1.51.0"
}, },
@@ -15543,6 +15645,10 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.1", "@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3" "tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
} }
}, },
"node_modules/mini-css-extract-plugin": { "node_modules/mini-css-extract-plugin": {
@@ -18350,11 +18456,11 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@@ -18364,20 +18470,26 @@
"react-is": "^16.6.0", "react-is": "^16.6.0",
"tiny-invariant": "^1.0.2", "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-router": "5.2.0", "react-router": "5.3.3",
"tiny-invariant": "^1.0.2", "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
} }
}, },
"node_modules/react-scripts": { "node_modules/react-scripts": {
@@ -20073,6 +20185,11 @@
"webpack": "^5.0.0" "webpack": "^5.0.0"
} }
}, },
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
},
"node_modules/styled-components": { "node_modules/styled-components": {
"version": "5.3.5", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
@@ -20312,9 +20429,9 @@
"dev": true "dev": true
}, },
"node_modules/tabbable": { "node_modules/tabbable": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.0.tgz", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
"integrity": "sha512-0uyt8wbP0P3T4rrsfYg/5Rg3cIJ8Shl1RJ54QMqYxm1TLdWqJD1u6+RQjr2Lor3wmfT7JRHkirIwy99ydBsyPg==" "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.0.15", "version": "3.0.15",
@@ -20701,9 +20818,9 @@
"dev": true "dev": true
}, },
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
}, },
"node_modules/tiny-warning": { "node_modules/tiny-warning": {
"version": "1.0.3", "version": "1.0.3",
@@ -21140,6 +21257,11 @@
"browser-process-hrtime": "^1.0.0" "browser-process-hrtime": "^1.0.0"
} }
}, },
"node_modules/w3c-keyname": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
},
"node_modules/w3c-xmlserializer": { "node_modules/w3c-xmlserializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
@@ -23496,6 +23618,76 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "dev": true
}, },
"@codemirror/autocomplete": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.0.2.tgz",
"integrity": "sha512-9PDjnllmXan/7Uax87KGORbxerDJ/cu10SB+n4Jz0zXMEvIh3+TGgZxhIvDOtaQ4jDBQEM7kHYW4vLdQB0DGZQ==",
"requires": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"@codemirror/commands": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.1.tgz",
"integrity": "sha512-iNHDByicYqQjs0Wo1MKGfqNbMYMyhS9WV6EwMVwsHXImlFemgEUC+c5X22bXKBStN3qnwg4fArNZM+gkv22baQ==",
"requires": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"@codemirror/language": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.0.tgz",
"integrity": "sha512-tabB0Ef/BflwoEmTB4a//WZ9P90UQyne9qWB9YFsmeS4bnEqSys7UpGk/da1URMXhyfuzWCwp+AQNMhvu8SfnA==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"@codemirror/lint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz",
"integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"@codemirror/search": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
"integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"@codemirror/state": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.0.tgz",
"integrity": "sha512-qbUr94DZTe6/V1VS7LDLz11rM/1t/nJxR1El4I6UaxDEdc0aZZvq6JCLJWiRmUf95NRAnDH6fhXn+PWp9wGCIg=="
},
"@codemirror/view": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
"integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
"requires": {
"@codemirror/state": "^6.0.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"@csstools/normalize.css": { "@csstools/normalize.css": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz",
@@ -24592,6 +24784,27 @@
} }
} }
}, },
"@lezer/common": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
"integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA=="
},
"@lezer/highlight": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
"integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@lezer/lr": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.1.0.tgz",
"integrity": "sha512-Iad04uVwk1PvSnj25mqj7zEEIRAsasbsTRmVzI0AUTs/+1Dz1//iYAaoLr7A+Xa7bZDfql5MKTxZmSlkYZD3Dg==",
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@lingui/babel-plugin-extract-messages": { "@lingui/babel-plugin-extract-messages": {
"version": "3.8.10", "version": "3.8.10",
"resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.10.tgz", "resolved": "https://registry.npmjs.org/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-3.8.10.tgz",
@@ -24776,9 +24989,9 @@
} }
}, },
"@lingui/core": { "@lingui/core": {
"version": "3.13.3", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.13.3.tgz", "resolved": "https://registry.npmjs.org/@lingui/core/-/core-3.14.0.tgz",
"integrity": "sha512-3rQDIC7PtPfUuZCSNfU0nziWNMlGk3JhpxENzGrlt1M8w5RHson89Mk1Ce/how+hWzFpumCQDWLDDhyRPpydbg==", "integrity": "sha512-ertREq9oi9B/umxpd/pInm9uFO8FLK2/0FXfDmMqvH5ydswWn/c9nY5YO4W1h4/8LWO45mewypOIyjoue4De1w==",
"requires": { "requires": {
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"make-plural": "^6.2.2", "make-plural": "^6.2.2",
@@ -24810,12 +25023,12 @@
} }
}, },
"@lingui/react": { "@lingui/react": {
"version": "3.13.3", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/@lingui/react/-/react-3.13.3.tgz", "resolved": "https://registry.npmjs.org/@lingui/react/-/react-3.14.0.tgz",
"integrity": "sha512-sCCI5xMcUY9b6w2lwbwy6iHpo1Fb9TDcjcHAD2KI5JueLH+WWQG66tIHiVAlSsQ+hmQ9Tt+f86H05JQEiDdIvg==", "integrity": "sha512-ow9Mtru7f0T2S9AwnPWRejppcucCW0LmoDR3P4wqHjL+eH5f8a6nxd2doxGieC91/2i4qqW88y4K/zXJxwRSQw==",
"requires": { "requires": {
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"@lingui/core": "^3.13.3" "@lingui/core": "^3.14.0"
} }
}, },
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
@@ -24851,30 +25064,24 @@
"dev": true "dev": true
}, },
"@patternfly/patternfly": { "@patternfly/patternfly": {
"version": "4.196.7", "version": "4.202.1",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.196.7.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.202.1.tgz",
"integrity": "sha512-hA7Oww411e1p0/IXjC1I+4/1NNis9V+NVBxfUIpRwyuLbCIDHBdtMu2qAPLdKxXjuibV9EE6ZdlT7ra/kcFuJQ==" "integrity": "sha512-cQiiPqmwJOm9onuTfLPQNRlpAZwDIJ/zVfDQeaFqMQyPJtxtKn3lkphz5xErY5dPs9rR4X94ytQ1I9pkVzaPJQ=="
}, },
"@patternfly/react-core": { "@patternfly/react-core": {
"version": "4.214.1", "version": "4.224.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.214.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-4.224.1.tgz",
"integrity": "sha512-XHEqXpnBEDyLVdAEDOYlGqFHnN43eNLSD5HABB99xO6541JV9MRnbxs0+v9iYnfhcKh/8bhA9ITXnUi3f2PEvg==", "integrity": "sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ==",
"requires": { "requires": {
"@patternfly/react-icons": "^4.65.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.64.1", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.66.1", "@patternfly/react-tokens": "^4.76.1",
"focus-trap": "6.2.2", "focus-trap": "6.9.2",
"react-dropzone": "9.0.0", "react-dropzone": "9.0.0",
"tippy.js": "5.1.2", "tippy.js": "5.1.2",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/react-icons": {
"version": "4.65.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.65.1.tgz",
"integrity": "sha512-CUYFRPztFkR7qrXq/0UAhLjeHd8FdjLe4jBjj8tfKc7OXwxDeZczqNFyRMATZpPaduTH7BU2r3OUjQrgAbquWg==",
"requires": {}
},
"tslib": { "tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -24883,35 +25090,29 @@
} }
}, },
"@patternfly/react-icons": { "@patternfly/react-icons": {
"version": "4.49.19", "version": "4.75.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.49.19.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.75.1.tgz",
"integrity": "sha512-Pr6JDDKWOnWChkifXKWglKEPo3Q+1CgiUTUrvk4ZbnD7mhq5e/TFxxInB9CPzi278bvnc2YlPyTjpaAcCN0yGw==", "integrity": "sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w==",
"requires": {} "requires": {}
}, },
"@patternfly/react-styles": { "@patternfly/react-styles": {
"version": "4.64.1", "version": "4.74.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.64.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-4.74.1.tgz",
"integrity": "sha512-+GxULkP2o5Vpr9w+J4NiGOGzhTfNniYzdPGEF/yC+oDoAXB6Q1HJyQnEj+kJH31xNvwmw3G3VFtwRLX4ZWr0oA==" "integrity": "sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A=="
}, },
"@patternfly/react-table": { "@patternfly/react-table": {
"version": "4.83.1", "version": "4.93.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.83.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-4.93.1.tgz",
"integrity": "sha512-mkq13x9funh+Nh2Uzj2ZQBOacNYc+a60yUAHZMXgNcljCJ3LTQUoYy6EonvYrqwSrpC7vj8nLt8+/XbDNc0Aig==", "integrity": "sha512-N/zHkNsY3X3yUXPg6COwdZKAFmTCbWm25qCY2aHjrXlIlE2OKWaYvVag0CcTwPiQhIuCumztr9Y2Uw9uvv0Fsw==",
"requires": { "requires": {
"@patternfly/react-core": "^4.214.1", "@patternfly/react-core": "^4.224.1",
"@patternfly/react-icons": "^4.65.1", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-styles": "^4.64.1", "@patternfly/react-styles": "^4.74.1",
"@patternfly/react-tokens": "^4.66.1", "@patternfly/react-tokens": "^4.76.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/react-icons": {
"version": "4.65.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-4.65.1.tgz",
"integrity": "sha512-CUYFRPztFkR7qrXq/0UAhLjeHd8FdjLe4jBjj8tfKc7OXwxDeZczqNFyRMATZpPaduTH7BU2r3OUjQrgAbquWg==",
"requires": {}
},
"tslib": { "tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@@ -24920,9 +25121,9 @@
} }
}, },
"@patternfly/react-tokens": { "@patternfly/react-tokens": {
"version": "4.66.1", "version": "4.76.1",
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.66.1.tgz", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz",
"integrity": "sha512-k0IWqpufM6ezT+3gWlEamqQ7LW9yi8e8cBBlude5IU8eIEqIG6AccwR1WNBEK1wCVWGwVxakLMdf0XBLl4k52Q==" "integrity": "sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw=="
}, },
"@pmmmwh/react-refresh-webpack-plugin": { "@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.4", "version": "0.5.4",
@@ -26395,8 +26596,7 @@
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
"dev": true
}, },
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@@ -26439,11 +26639,24 @@
"dev": true "dev": true
}, },
"axios": { "axios": {
"version": "0.22.0", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.22.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-Z0U3uhqQeg1oNcihswf4ZD57O3NrR1+ZXhxaROaWpDmsDTx7T2HNBV2ulBtie2hwJptu8UvgnJoK+BIqdzh/1w==", "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": { "requires": {
"follow-redirects": "^1.14.4" "follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
} }
}, },
"axobject-query": { "axobject-query": {
@@ -27240,9 +27453,18 @@
} }
}, },
"codemirror": { "codemirror": {
"version": "5.65.4", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.4.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-tytrSm5Rh52b6j36cbDXN+FHwHCl9aroY4BrDZB2NFFL3Wjfq9nuYVLFFhaOYOczKAg3JXTr8BuT8LcE5QY4Iw==" "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"requires": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
}, },
"collect-v8-coverage": { "collect-v8-coverage": {
"version": "1.0.1", "version": "1.0.1",
@@ -27285,7 +27507,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": { "requires": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
@@ -27471,6 +27692,11 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true "dev": true
}, },
"crelt": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
},
"cross-spawn": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -28220,8 +28446,7 @@
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
"dev": true
}, },
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
@@ -29827,17 +30052,17 @@
"dev": true "dev": true
}, },
"focus-trap": { "focus-trap": {
"version": "6.2.2", "version": "6.9.2",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.2.2.tgz", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.2.tgz",
"integrity": "sha512-qWovH9+LGoKqREvJaTCzJyO0hphQYGz+ap5Hc4NqXHNhZBdxCi5uBPPcaOUw66fHmzXLVwvETLvFgpwPILqKpg==", "integrity": "sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==",
"requires": { "requires": {
"tabbable": "^5.1.4" "tabbable": "^5.3.2"
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.8", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
}, },
"form-data": { "form-data": {
"version": "3.0.1", "version": "3.0.1",
@@ -30916,7 +31141,7 @@
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
}, },
"isexe": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
@@ -34076,14 +34301,12 @@
"mime-db": { "mime-db": {
"version": "1.51.0", "version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
"integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
"dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.34", "version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dev": true,
"requires": { "requires": {
"mime-db": "1.51.0" "mime-db": "1.51.0"
} }
@@ -36113,11 +36336,11 @@
"dev": true "dev": true
}, },
"react-router": { "react-router": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@@ -36130,15 +36353,15 @@
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-router": "5.2.0", "react-router": "5.3.3",
"tiny-invariant": "^1.0.2", "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
} }
@@ -37407,6 +37630,11 @@
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
},
"styled-components": { "styled-components": {
"version": "5.3.5", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.5.tgz",
@@ -37600,9 +37828,9 @@
"dev": true "dev": true
}, },
"tabbable": { "tabbable": {
"version": "5.2.0", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.0.tgz", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
"integrity": "sha512-0uyt8wbP0P3T4rrsfYg/5Rg3cIJ8Shl1RJ54QMqYxm1TLdWqJD1u6+RQjr2Lor3wmfT7JRHkirIwy99ydBsyPg==" "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
}, },
"tailwindcss": { "tailwindcss": {
"version": "3.0.15", "version": "3.0.15",
@@ -37873,9 +38101,9 @@
"dev": true "dev": true
}, },
"tiny-invariant": { "tiny-invariant": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
}, },
"tiny-warning": { "tiny-warning": {
"version": "1.0.3", "version": "1.0.3",
@@ -38220,6 +38448,11 @@
"browser-process-hrtime": "^1.0.0" "browser-process-hrtime": "^1.0.0"
} }
}, },
"w3c-keyname": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
},
"w3c-xmlserializer": { "w3c-xmlserializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",

View File

@@ -6,15 +6,15 @@
"node": ">=16.13.1" "node": ">=16.13.1"
}, },
"dependencies": { "dependencies": {
"@lingui/react": "3.13.3", "@lingui/react": "3.14.0",
"@patternfly/patternfly": "4.196.7", "@patternfly/patternfly": "4.202.1",
"@patternfly/react-core": "^4.201.0", "@patternfly/react-core": "^4.221.3",
"@patternfly/react-icons": "4.49.19", "@patternfly/react-icons": "4.75.1",
"@patternfly/react-table": "4.83.1", "@patternfly/react-table": "4.93.1",
"ace-builds": "^1.6.0", "ace-builds": "^1.6.0",
"ansi-to-html": "0.7.2", "ansi-to-html": "0.7.2",
"axios": "0.22.0", "axios": "0.27.2",
"codemirror": "^5.65.4", "codemirror": "^6.0.1",
"d3": "7.4.4", "d3": "7.4.4",
"dagre": "^0.8.4", "dagre": "^0.8.4",
"dompurify": "2.3.8", "dompurify": "2.3.8",
@@ -28,7 +28,7 @@
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.3.3",
"react-virtualized": "^9.21.1", "react-virtualized": "^9.21.1",
"rrule": "2.7.0", "rrule": "2.7.0",
"styled-components": "5.3.5" "styled-components": "5.3.5"

View File

@@ -26,13 +26,6 @@ function AdHocCommands({
const [isWizardOpen, setIsWizardOpen] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false);
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext); const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const verbosityOptions = [
{ value: '0', key: '0', label: t`0 (Normal)` },
{ value: '1', key: '1', label: t`1 (Verbose)` },
{ value: '2', key: '2', label: t`2 (More Verbose)` },
{ value: '3', key: '3', label: t`3 (Debug)` },
{ value: '4', key: '4', label: t`4 (Connection Debug)` },
];
useEffect(() => { useEffect(() => {
if (isKebabified) { if (isKebabified) {
onKebabModalChange(isWizardOpen); onKebabModalChange(isWizardOpen);
@@ -159,7 +152,6 @@ function AdHocCommands({
adHocItems={adHocItems} adHocItems={adHocItems}
organizationId={organizationId} organizationId={organizationId}
moduleOptions={moduleOptions} moduleOptions={moduleOptions}
verbosityOptions={verbosityOptions}
credentialTypeId={credentialTypeId} credentialTypeId={credentialTypeId}
onCloseWizard={() => setIsWizardOpen(false)} onCloseWizard={() => setIsWizardOpen(false)}
onLaunch={handleSubmit} onLaunch={handleSubmit}

View File

@@ -3,13 +3,13 @@ import { t } from '@lingui/macro';
import { withFormik, useFormikContext } from 'formik'; import { withFormik, useFormikContext } from 'formik';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { VERBOSITY } from 'components/VerbositySelectField';
import Wizard from '../Wizard'; import Wizard from '../Wizard';
import useAdHocLaunchSteps from './useAdHocLaunchSteps'; import useAdHocLaunchSteps from './useAdHocLaunchSteps';
function AdHocCommandsWizard({ function AdHocCommandsWizard({
onLaunch, onLaunch,
moduleOptions, moduleOptions,
verbosityOptions,
onCloseWizard, onCloseWizard,
credentialTypeId, credentialTypeId,
organizationId, organizationId,
@@ -18,7 +18,6 @@ function AdHocCommandsWizard({
const { steps, validateStep, visitStep, visitAllSteps } = useAdHocLaunchSteps( const { steps, validateStep, visitStep, visitAllSteps } = useAdHocLaunchSteps(
moduleOptions, moduleOptions,
verbosityOptions,
organizationId, organizationId,
credentialTypeId credentialTypeId
); );
@@ -57,13 +56,13 @@ function AdHocCommandsWizard({
} }
const FormikApp = withFormik({ const FormikApp = withFormik({
mapPropsToValues({ adHocItems, verbosityOptions }) { mapPropsToValues({ adHocItems }) {
const adHocItemStrings = adHocItems.map((item) => item.name).join(', '); const adHocItemStrings = adHocItems.map((item) => item.name).join(', ');
return { return {
limit: adHocItemStrings || 'all', limit: adHocItemStrings || 'all',
credentials: [], credentials: [],
module_args: '', module_args: '',
verbosity: verbosityOptions[0].value, verbosity: VERBOSITY()[0],
forks: 0, forks: 0,
diff_mode: false, diff_mode: false,
become_enabled: '', become_enabled: '',
@@ -79,7 +78,6 @@ const FormikApp = withFormik({
FormikApp.propTypes = { FormikApp.propTypes = {
onLaunch: PropTypes.func.isRequired, onLaunch: PropTypes.func.isRequired,
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired, moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
onCloseWizard: PropTypes.func.isRequired, onCloseWizard: PropTypes.func.isRequired,
credentialTypeId: PropTypes.number.isRequired, credentialTypeId: PropTypes.number.isRequired,
}; };

View File

@@ -13,13 +13,6 @@ jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/ExecutionEnvironments'); jest.mock('../../api/models/ExecutionEnvironments');
jest.mock('../../api/models/Root'); jest.mock('../../api/models/Root');
const verbosityOptions = [
{ value: '0', key: '0', label: '0 (Normal)' },
{ value: '1', key: '1', label: '1 (Verbose)' },
{ value: '2', key: '2', label: '2 (More Verbose)' },
{ value: '3', key: '3', label: '3 (Debug)' },
{ value: '4', key: '4', label: '4 (Connection Debug)' },
];
const moduleOptions = [ const moduleOptions = [
['command', 'command'], ['command', 'command'],
['shell', 'shell'], ['shell', 'shell'],
@@ -44,7 +37,6 @@ describe('<AdHocCommandsWizard/>', () => {
adHocItems={adHocItems} adHocItems={adHocItems}
onLaunch={onLaunch} onLaunch={onLaunch}
moduleOptions={moduleOptions} moduleOptions={moduleOptions}
verbosityOptions={verbosityOptions}
onCloseWizard={() => {}} onCloseWizard={() => {}}
credentialTypeId={1} credentialTypeId={1}
organizationId={1} organizationId={1}

View File

@@ -7,6 +7,7 @@ import { Form, FormGroup, Switch, Checkbox } from '@patternfly/react-core';
import styled from 'styled-components'; import styled from 'styled-components';
import { required } from 'util/validators'; import { required } from 'util/validators';
import useBrandName from 'hooks/useBrandName'; import useBrandName from 'hooks/useBrandName';
import { VerbositySelectField } from 'components/VerbositySelectField';
import AnsibleSelect from '../AnsibleSelect'; import AnsibleSelect from '../AnsibleSelect';
import FormField from '../FormField'; import FormField from '../FormField';
import { VariablesField } from '../CodeEditor'; import { VariablesField } from '../CodeEditor';
@@ -21,7 +22,7 @@ const TooltipWrapper = styled.div`
text-align: left; text-align: left;
`; `;
function AdHocDetailsStep({ verbosityOptions, moduleOptions }) { function AdHocDetailsStep({ moduleOptions }) {
const brandName = useBrandName(); const brandName = useBrandName();
const [moduleNameField, moduleNameMeta, moduleNameHelpers] = useField({ const [moduleNameField, moduleNameMeta, moduleNameHelpers] = useField({
name: 'module_name', name: 'module_name',
@@ -32,7 +33,7 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
const [diffModeField, , diffModeHelpers] = useField('diff_mode'); const [diffModeField, , diffModeHelpers] = useField('diff_mode');
const [becomeEnabledField, , becomeEnabledHelpers] = const [becomeEnabledField, , becomeEnabledHelpers] =
useField('become_enabled'); useField('become_enabled');
const [verbosityField, verbosityMeta, verbosityHelpers] = useField({ const [, verbosityMeta] = useField({
name: 'verbosity', name: 'verbosity',
validate: required(null), validate: required(null),
}); });
@@ -122,33 +123,16 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
) )
} }
/> />
<FormGroup
<VerbositySelectField
fieldId="verbosity" fieldId="verbosity"
aria-label={t`select verbosity`} tooltip={t`These are the verbosity levels for standard out of the command run that are supported.`}
label={t`Verbosity`} isValid={
isRequired
validated={
!verbosityMeta.touched || !verbosityMeta.error !verbosityMeta.touched || !verbosityMeta.error
? 'default' ? 'default'
: 'error' : 'error'
} }
helperTextInvalid={verbosityMeta.error}
labelIcon={
<Popover
content={t`These are the verbosity levels for standard out of the command run that are supported.`}
/> />
}
>
<AnsibleSelect
{...verbosityField}
isValid={!verbosityMeta.touched || !verbosityMeta.error}
id="verbosity"
data={verbosityOptions || []}
onChange={(event, value) => {
verbosityHelpers.setValue(parseInt(value, 10));
}}
/>
</FormGroup>
<FormField <FormField
id="limit" id="limit"
name="limit" name="limit"
@@ -296,7 +280,6 @@ function AdHocDetailsStep({ verbosityOptions, moduleOptions }) {
AdHocDetailsStep.propTypes = { AdHocDetailsStep.propTypes = {
moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired, moduleOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
}; };
export default AdHocDetailsStep; export default AdHocDetailsStep;

View File

@@ -3,6 +3,7 @@ import { t } from '@lingui/macro';
import { Tooltip } from '@patternfly/react-core'; import { Tooltip } from '@patternfly/react-core';
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons'; import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import { VERBOSITY } from '../VerbositySelectField';
import { toTitleCase } from '../../util/strings'; import { toTitleCase } from '../../util/strings';
import { VariablesDetail } from '../CodeEditor'; import { VariablesDetail } from '../CodeEditor';
import { jsonToYaml } from '../../util/yaml'; import { jsonToYaml } from '../../util/yaml';
@@ -21,7 +22,7 @@ const ErrorMessageWrapper = styled.div`
margin-bottom: 10px; margin-bottom: 10px;
`; `;
function AdHocPreviewStep({ hasErrors, values }) { function AdHocPreviewStep({ hasErrors, values }) {
const { credential, execution_environment, extra_vars } = values; const { credential, execution_environment, extra_vars, verbosity } = values;
const items = Object.entries(values); const items = Object.entries(values);
return ( return (
@@ -44,6 +45,7 @@ function AdHocPreviewStep({ hasErrors, values }) {
key !== 'extra_vars' && key !== 'extra_vars' &&
key !== 'execution_environment' && key !== 'execution_environment' &&
key !== 'credentials' && key !== 'credentials' &&
key !== 'verbosity' &&
!key.startsWith('credential_passwords') && ( !key.startsWith('credential_passwords') && (
<Detail key={key} label={toTitleCase(key)} value={value} /> <Detail key={key} label={toTitleCase(key)} value={value} />
) )
@@ -57,6 +59,9 @@ function AdHocPreviewStep({ hasErrors, values }) {
value={execution_environment[0]?.name} value={execution_environment[0]?.name}
/> />
)} )}
{verbosity && (
<Detail label={t`Verbosity`} value={VERBOSITY()[values.verbosity]} />
)}
{extra_vars && ( {extra_vars && (
<VariablesDetail <VariablesDetail
value={jsonToYaml(JSON.stringify(extra_vars))} value={jsonToYaml(JSON.stringify(extra_vars))}

View File

@@ -5,11 +5,7 @@ import StepName from '../LaunchPrompt/steps/StepName';
import AdHocDetailsStep from './AdHocDetailsStep'; import AdHocDetailsStep from './AdHocDetailsStep';
const STEP_ID = 'details'; const STEP_ID = 'details';
export default function useAdHocDetailsStep( export default function useAdHocDetailsStep(visited, moduleOptions) {
visited,
moduleOptions,
verbosityOptions
) {
const { values, touched, setFieldError } = useFormikContext(); const { values, touched, setFieldError } = useFormikContext();
const hasError = () => { const hasError = () => {
@@ -39,12 +35,7 @@ export default function useAdHocDetailsStep(
{t`Details`} {t`Details`}
</StepName> </StepName>
), ),
component: ( component: <AdHocDetailsStep moduleOptions={moduleOptions} />,
<AdHocDetailsStep
moduleOptions={moduleOptions}
verbosityOptions={verbosityOptions}
/>
),
enableNext: true, enableNext: true,
nextButtonText: t`Next`, nextButtonText: t`Next`,
}, },

View File

@@ -24,7 +24,6 @@ function showCredentialPasswordsStep(credential) {
export default function useAdHocLaunchSteps( export default function useAdHocLaunchSteps(
moduleOptions, moduleOptions,
verbosityOptions,
organizationId, organizationId,
credentialTypeId credentialTypeId
) { ) {
@@ -32,7 +31,7 @@ export default function useAdHocLaunchSteps(
const [visited, setVisited] = useState({}); const [visited, setVisited] = useState({});
const steps = [ const steps = [
useAdHocDetailsStep(visited, moduleOptions, verbosityOptions), useAdHocDetailsStep(visited, moduleOptions),
useAdHocExecutionEnvironmentStep(organizationId), useAdHocExecutionEnvironmentStep(organizationId),
useAdHocCredentialStep(visited, credentialTypeId), useAdHocCredentialStep(visited, credentialTypeId),
useCredentialPasswordsStep( useCredentialPasswordsStep(

View File

@@ -46,7 +46,9 @@ function AnsibleSelect({
value={option.value} value={option.value}
label={option.label} label={option.label}
isDisabled={option.isDisabled} isDisabled={option.isDisabled}
/> >
{option.label}
</FormSelectOption>
))} ))}
</FormSelect> </FormSelect>
); );

View File

@@ -113,48 +113,6 @@ describe('LaunchButton', () => {
expect(history.location.pathname).toEqual('/jobs/9000/output'); expect(history.location.pathname).toEqual('/jobs/9000/output');
}); });
test('should disable button to prevent duplicate clicks', async () => {
WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({
data: {
can_start_without_user_input: true,
},
});
const history = createMemoryHistory({
initialEntries: ['/jobs/9000'],
});
WorkflowJobTemplatesAPI.launch.mockImplementation(async () => {
// return asynchronously so isLaunching isn't set back to false in the
// same tick
await new Promise((resolve) => setTimeout(resolve, 10));
return {
data: {
id: 9000,
},
};
});
const wrapper = mountWithContexts(
<LaunchButton
resource={{
id: 1,
type: 'workflow_job_template',
}}
>
{({ handleLaunch, isLaunching }) => (
<button type="submit" onClick={handleLaunch} disabled={isLaunching} />
)}
</LaunchButton>,
{
context: {
router: { history },
},
}
);
const button = wrapper.find('button');
await act(() => button.prop('onClick')());
wrapper.update();
expect(wrapper.find('button').prop('disabled')).toEqual(false);
});
test('should relaunch job correctly', async () => { test('should relaunch job correctly', async () => {
JobsAPI.readRelaunch.mockResolvedValue({ JobsAPI.readRelaunch.mockResolvedValue({
data: { data: {

View File

@@ -9,6 +9,7 @@ import { TagMultiSelect } from '../../MultiSelect';
import AnsibleSelect from '../../AnsibleSelect'; import AnsibleSelect from '../../AnsibleSelect';
import { VariablesField } from '../../CodeEditor'; import { VariablesField } from '../../CodeEditor';
import Popover from '../../Popover'; import Popover from '../../Popover';
import { VerbositySelectField } from '../../VerbositySelectField';
const FieldHeader = styled.div` const FieldHeader = styled.div`
display: flex; display: flex;
@@ -57,7 +58,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
aria-label={t`Job Tags`} aria-label={t`Job Tags`}
tooltip={t`Tags are useful when you have a large tooltip={t`Tags are useful when you have a large
playbook, and you want to run a specific part of a play or task. playbook, and you want to run a specific part of a play or task.
Use commas to separate multiple tags. Refer to Ansible Tower Use commas to separate multiple tags. Refer to Ansible Controller
documentation for details on the usage of tags.`} documentation for details on the usage of tags.`}
/> />
)} )}
@@ -69,7 +70,7 @@ function OtherPromptsStep({ launchConfig, variablesMode, onVarModeChange }) {
aria-label={t`Skip Tags`} aria-label={t`Skip Tags`}
tooltip={t`Skip tags are useful when you have a large tooltip={t`Skip tags are useful when you have a large
playbook, and you want to skip specific parts of a play or task. playbook, and you want to skip specific parts of a play or task.
Use commas to separate multiple tags. Refer to Ansible Tower Use commas to separate multiple tags. Refer to Ansible Controller
documentation for details on the usage of tags.`} documentation for details on the usage of tags.`}
/> />
)} )}
@@ -129,36 +130,16 @@ function JobTypeField() {
} }
function VerbosityField() { function VerbosityField() {
const [field, meta, helpers] = useField('verbosity'); const [, meta] = useField('verbosity');
const options = [
{ value: '0', key: '0', label: t`0 (Normal)` },
{ value: '1', key: '1', label: t`1 (Verbose)` },
{ value: '2', key: '2', label: t`2 (More Verbose)` },
{ value: '3', key: '3', label: t`3 (Debug)` },
{ value: '4', key: '4', label: t`4 (Connection Debug)` },
];
const isValid = !(meta.touched && meta.error); const isValid = !(meta.touched && meta.error);
return ( return (
<FormGroup <VerbositySelectField
fieldId="prompt-verbosity" fieldId="prompt-verbosity"
validated={isValid ? 'default' : 'error'} tooltip={t`Control the level of output ansible
label={t`Verbosity`}
labelIcon={
<Popover
content={t`Control the level of output ansible
will produce as the playbook executes.`} will produce as the playbook executes.`}
isValid={isValid ? 'default' : 'error'}
/> />
}
>
<AnsibleSelect
id="prompt-verbosity"
data={options}
{...field}
onChange={(event, value) => helpers.setValue(value)}
/>
</FormGroup>
); );
} }

View File

@@ -85,7 +85,7 @@ describe('OtherPromptsStep', () => {
expect(wrapper.find('VerbosityField')).toHaveLength(1); expect(wrapper.find('VerbosityField')).toHaveLength(1);
expect( expect(
wrapper.find('VerbosityField AnsibleSelect').prop('data') wrapper.find('VerbosityField AnsibleSelect').prop('data')
).toHaveLength(5); ).toHaveLength(6);
}); });
test('should render show changes toggle', async () => { test('should render show changes toggle', async () => {

View File

@@ -349,7 +349,7 @@ function HostFilterLookup({
content={t`Populate the hosts for this inventory by using a search content={t`Populate the hosts for this inventory by using a search
filter. Example: ansible_facts__ansible_distribution:"RedHat". filter. Example: ansible_facts__ansible_distribution:"RedHat".
Refer to the documentation for further syntax and Refer to the documentation for further syntax and
examples. Refer to the Ansible Tower documentation for further syntax and examples. Refer to the Ansible Controller documentation for further syntax and
examples.`} examples.`}
/> />
} }

View File

@@ -14,6 +14,7 @@ import PromptProjectDetail from './PromptProjectDetail';
import PromptInventorySourceDetail from './PromptInventorySourceDetail'; import PromptInventorySourceDetail from './PromptInventorySourceDetail';
import PromptJobTemplateDetail from './PromptJobTemplateDetail'; import PromptJobTemplateDetail from './PromptJobTemplateDetail';
import PromptWFJobTemplateDetail from './PromptWFJobTemplateDetail'; import PromptWFJobTemplateDetail from './PromptWFJobTemplateDetail';
import { VERBOSITY } from '../VerbositySelectField';
const PromptTitle = styled(Title)` const PromptTitle = styled(Title)`
margin-top: var(--pf-global--spacer--xl); margin-top: var(--pf-global--spacer--xl);
@@ -93,14 +94,6 @@ function PromptDetail({
overrides = {}, overrides = {},
workflowNode = false, workflowNode = false,
}) { }) {
const VERBOSITY = {
0: t`0 (Normal)`,
1: t`1 (Verbose)`,
2: t`2 (More Verbose)`,
3: t`3 (Debug)`,
4: t`4 (Connection Debug)`,
};
const details = omitOverrides(resource, overrides, launchConfig.defaults); const details = omitOverrides(resource, overrides, launchConfig.defaults);
details.type = overrides?.nodeType || details.type; details.type = overrides?.nodeType || details.type;
const hasOverrides = Object.keys(overrides).length > 0; const hasOverrides = Object.keys(overrides).length > 0;
@@ -226,7 +219,7 @@ function PromptDetail({
launchConfig.ask_verbosity_on_launch ? ( launchConfig.ask_verbosity_on_launch ? (
<Detail <Detail
label={t`Verbosity`} label={t`Verbosity`}
value={VERBOSITY[overrides.verbosity]} value={VERBOSITY()[overrides.verbosity]}
/> />
) : null} ) : null}
{launchConfig.ask_tags_on_launch && ( {launchConfig.ask_tags_on_launch && (

View File

@@ -13,6 +13,7 @@ import { VariablesDetail } from '../CodeEditor';
import CredentialChip from '../CredentialChip'; import CredentialChip from '../CredentialChip';
import ChipGroup from '../ChipGroup'; import ChipGroup from '../ChipGroup';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail'; import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
import { VERBOSITY } from '../VerbositySelectField';
function PromptInventorySourceDetail({ resource }) { function PromptInventorySourceDetail({ resource }) {
const { const {
@@ -32,14 +33,6 @@ function PromptInventorySourceDetail({ resource }) {
verbosity, verbosity,
} = resource; } = resource;
const VERBOSITY = {
0: t`0 (Normal)`,
1: t`1 (Verbose)`,
2: t`2 (More Verbose)`,
3: t`3 (Debug)`,
4: t`4 (Connection Debug)`,
};
let optionsList = ''; let optionsList = '';
if ( if (
overwrite || overwrite ||
@@ -115,7 +108,7 @@ function PromptInventorySourceDetail({ resource }) {
executionEnvironment={summary_fields?.execution_environment} executionEnvironment={summary_fields?.execution_environment}
/> />
<Detail label={t`Inventory File`} value={source_path} /> <Detail label={t`Inventory File`} value={source_path} />
<Detail label={t`Verbosity`} value={VERBOSITY[verbosity]} /> <Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} />
<Detail <Detail
label={t`Cache Timeout`} label={t`Cache Timeout`}
value={`${update_cache_timeout} ${t`Seconds`}`} value={`${update_cache_timeout} ${t`Seconds`}`}

View File

@@ -15,6 +15,7 @@ import Sparkline from '../Sparkline';
import { Detail, DeletedDetail } from '../DetailList'; import { Detail, DeletedDetail } from '../DetailList';
import { VariablesDetail } from '../CodeEditor'; import { VariablesDetail } from '../CodeEditor';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail'; import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
import { VERBOSITY } from '../VerbositySelectField';
function PromptJobTemplateDetail({ resource }) { function PromptJobTemplateDetail({ resource }) {
const { const {
@@ -42,14 +43,6 @@ function PromptJobTemplateDetail({ resource }) {
custom_virtualenv, custom_virtualenv,
} = resource; } = resource;
const VERBOSITY = {
0: t`0 (Normal)`,
1: t`1 (Verbose)`,
2: t`2 (More Verbose)`,
3: t`3 (Debug)`,
4: t`4 (Connection Debug)`,
};
let optionsList = ''; let optionsList = '';
if ( if (
become_enabled || become_enabled ||
@@ -153,7 +146,7 @@ function PromptJobTemplateDetail({ resource }) {
<Detail label={t`Playbook`} value={playbook} /> <Detail label={t`Playbook`} value={playbook} />
<Detail label={t`Forks`} value={forks || '0'} /> <Detail label={t`Forks`} value={forks || '0'} />
<Detail label={t`Limit`} value={limit} /> <Detail label={t`Limit`} value={limit} />
<Detail label={t`Verbosity`} value={VERBOSITY[verbosity]} /> <Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} />
{typeof diff_mode === 'boolean' && ( {typeof diff_mode === 'boolean' && (
<Detail label={t`Show Changes`} value={diff_mode ? t`On` : t`Off`} /> <Detail label={t`Show Changes`} value={diff_mode ? t`On` : t`Off`} />
)} )}

View File

@@ -11,6 +11,7 @@ import { formatDateString } from 'util/dates';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import { JobTemplatesAPI, SchedulesAPI, WorkflowJobTemplatesAPI } from 'api'; import { JobTemplatesAPI, SchedulesAPI, WorkflowJobTemplatesAPI } from 'api';
import { parseVariableField, jsonToYaml } from 'util/yaml'; import { parseVariableField, jsonToYaml } from 'util/yaml';
import { useConfig } from 'contexts/Config';
import AlertModal from '../../AlertModal'; import AlertModal from '../../AlertModal';
import { CardBody, CardActionsRow } from '../../Card'; import { CardBody, CardActionsRow } from '../../Card';
import ContentError from '../../ContentError'; import ContentError from '../../ContentError';
@@ -23,6 +24,8 @@ import DeleteButton from '../../DeleteButton';
import ErrorDetail from '../../ErrorDetail'; import ErrorDetail from '../../ErrorDetail';
import ChipGroup from '../../ChipGroup'; import ChipGroup from '../../ChipGroup';
import { VariablesDetail } from '../../CodeEditor'; import { VariablesDetail } from '../../CodeEditor';
import { VERBOSITY } from '../../VerbositySelectField';
import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
const PromptDivider = styled(Divider)` const PromptDivider = styled(Divider)`
margin-top: var(--pf-global--spacer--lg); margin-top: var(--pf-global--spacer--lg);
@@ -38,7 +41,6 @@ const PromptTitle = styled(Title)`
const PromptDetailList = styled(DetailList)` const PromptDetailList = styled(DetailList)`
padding: 0px 20px; padding: 0px 20px;
`; `;
function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) { function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
const { const {
id, id,
@@ -66,14 +68,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
const history = useHistory(); const history = useHistory();
const { pathname } = useLocation(); const { pathname } = useLocation();
const pathRoot = pathname.substr(0, pathname.indexOf('schedules')); const pathRoot = pathname.substr(0, pathname.indexOf('schedules'));
const config = useConfig();
const VERBOSITY = {
0: t`0 (Normal)`,
1: t`1 (Verbose)`,
2: t`2 (More Verbose)`,
3: t`3 (Debug)`,
4: t`4 (Connection Debug)`,
};
const { const {
request: deleteSchedule, request: deleteSchedule,
@@ -216,7 +211,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
const showLimitDetail = ask_limit_on_launch && limit; const showLimitDetail = ask_limit_on_launch && limit;
const showJobTypeDetail = ask_job_type_on_launch && job_type; const showJobTypeDetail = ask_job_type_on_launch && job_type;
const showSCMBranchDetail = ask_scm_branch_on_launch && scm_branch; const showSCMBranchDetail = ask_scm_branch_on_launch && scm_branch;
const showVerbosityDetail = ask_verbosity_on_launch && VERBOSITY[verbosity]; const showVerbosityDetail = ask_verbosity_on_launch && VERBOSITY()[verbosity];
const showPromptedFields = const showPromptedFields =
showCredentialsDetail || showCredentialsDetail ||
@@ -267,7 +262,11 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
value={formatDateString(next_run, timezone)} value={formatDateString(next_run, timezone)}
/> />
<Detail label={t`Last Run`} value={formatDateString(dtend, timezone)} /> <Detail label={t`Last Run`} value={formatDateString(dtend, timezone)} />
<Detail label={t`Local Time Zone`} value={timezone} /> <Detail
label={t`Local Time Zone`}
value={timezone}
helpText={helpText.localTimeZone(config)}
/>
<Detail label={t`Repeat Frequency`} value={repeatFrequency} /> <Detail label={t`Repeat Frequency`} value={repeatFrequency} />
{hasDaysToKeepField ? ( {hasDaysToKeepField ? (
<Detail label={t`Days of Data to Keep`} value={daysToKeep} /> <Detail label={t`Days of Data to Keep`} value={daysToKeep} />
@@ -313,7 +312,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
/> />
)} )}
{ask_verbosity_on_launch && ( {ask_verbosity_on_launch && (
<Detail label={t`Verbosity`} value={VERBOSITY[verbosity]} /> <Detail label={t`Verbosity`} value={VERBOSITY()[verbosity]} />
)} )}
{ask_scm_branch_on_launch && ( {ask_scm_branch_on_launch && (
<Detail label={t`Source Control Branch`} value={scm_branch} /> <Detail label={t`Source Control Branch`} value={scm_branch} />

View File

@@ -164,6 +164,9 @@ describe('<ScheduleDetail />', () => {
expect( expect(
wrapper.find('Detail[label="Local Time Zone"]').find('dd').text() wrapper.find('Detail[label="Local Time Zone"]').find('dd').text()
).toBe('America/New_York'); ).toBe('America/New_York');
expect(
wrapper.find('Detail[label="Local Time Zone"]').prop('helpText')
).toBeDefined();
expect(wrapper.find('Detail[label="Repeat Frequency"]').length).toBe(1); expect(wrapper.find('Detail[label="Repeat Frequency"]').length).toBe(1);
expect(wrapper.find('Detail[label="Created"]').length).toBe(1); expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1); expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1);

View File

@@ -14,12 +14,13 @@ import {
// To be removed once UI completes complex schedules // To be removed once UI completes complex schedules
Alert, Alert,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { Config } from 'contexts/Config'; import { Config, useConfig } from 'contexts/Config';
import { SchedulesAPI } from 'api'; import { SchedulesAPI } from 'api';
import { dateToInputDateTime } from 'util/dates'; import { dateToInputDateTime } from 'util/dates';
import useRequest from 'hooks/useRequest'; import useRequest from 'hooks/useRequest';
import { required } from 'util/validators'; import { required } from 'util/validators';
import { parseVariableField } from 'util/yaml'; import { parseVariableField } from 'util/yaml';
import Popover from '../../Popover';
import AnsibleSelect from '../../AnsibleSelect'; import AnsibleSelect from '../../AnsibleSelect';
import ContentError from '../../ContentError'; import ContentError from '../../ContentError';
import ContentLoading from '../../ContentLoading'; import ContentLoading from '../../ContentLoading';
@@ -33,6 +34,7 @@ import FrequencyDetailSubform from './FrequencyDetailSubform';
import SchedulePromptableFields from './SchedulePromptableFields'; import SchedulePromptableFields from './SchedulePromptableFields';
import DateTimePicker from './DateTimePicker'; import DateTimePicker from './DateTimePicker';
import buildRuleObj from './buildRuleObj'; import buildRuleObj from './buildRuleObj';
import helpText from '../../../screens/Template/shared/JobTemplate.helptext';
const NUM_DAYS_PER_FREQUENCY = { const NUM_DAYS_PER_FREQUENCY = {
week: 7, week: 7,
@@ -118,6 +120,9 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions, zoneLinks }) {
} else if (timezoneMessage) { } else if (timezoneMessage) {
timezoneValidatedStatus = 'warning'; timezoneValidatedStatus = 'warning';
} }
const config = useConfig();
return ( return (
<> <>
<FormField <FormField
@@ -147,6 +152,7 @@ function ScheduleFormFields({ hasDaysToKeepField, zoneOptions, zoneLinks }) {
validated={timezoneValidatedStatus} validated={timezoneValidatedStatus}
label={t`Local time zone`} label={t`Local time zone`}
helperText={timezoneMessage} helperText={timezoneMessage}
labelIcon={<Popover content={helpText.localTimeZone(config)} />}
> >
<AnsibleSelect <AnsibleSelect
id="schedule-timezone" id="schedule-timezone"

View File

@@ -91,6 +91,9 @@ const defaultFieldsVisible = () => {
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Start date/time"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Start date/time"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Local time zone"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Local time zone"]').length).toBe(1);
expect(
wrapper.find('FormGroup[label="Local time zone"]').find('HelpIcon').length
).toBe(1);
expect(wrapper.find('FormGroup[label="Run frequency"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Run frequency"]').length).toBe(1);
}; };

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { FormGroup } from '@patternfly/react-core';
import Popover from 'components/Popover';
import AnsibleSelect from 'components/AnsibleSelect';
import FieldWithPrompt from 'components/FieldWithPrompt';
const VERBOSITY = () => ({
0: t`0 (Normal)`,
1: t`1 (Verbose)`,
2: t`2 (More Verbose)`,
3: t`3 (Debug)`,
4: t`4 (Connection Debug)`,
5: t`5 (WinRM Debug)`,
});
function VerbositySelectField({
fieldId,
promptId,
promptName,
tooltip,
isValid,
}) {
const VERBOSE_OPTIONS = Object.entries(VERBOSITY()).map(([k, v]) => ({
key: `${k}`,
value: `${k}`,
label: v,
}));
const [verbosityField, , verbosityHelpers] = useField('verbosity');
return promptId ? (
<FieldWithPrompt
fieldId={fieldId}
label={t`Verbosity`}
promptId={promptId}
promptName={promptName}
tooltip={tooltip}
>
<AnsibleSelect id={fieldId} data={VERBOSE_OPTIONS} {...verbosityField} />
</FieldWithPrompt>
) : (
<FormGroup
fieldId={fieldId}
validated={isValid ? 'default' : 'error'}
label={t`Verbosity`}
labelIcon={<Popover content={tooltip} />}
>
<AnsibleSelect
id={fieldId}
data={VERBOSE_OPTIONS}
{...verbosityField}
onChange={(event, value) => verbosityHelpers.setValue(value)}
/>
</FormGroup>
);
}
export { VerbositySelectField, VERBOSITY };

View File

@@ -0,0 +1 @@
export { VERBOSITY, VerbositySelectField } from './VerbositySelectField';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -418,7 +418,7 @@ describe('<CredentialForm />', () => {
).toBe(false); ).toBe(false);
expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Credential Type"]').length).toBe(1);
expect( expect(
wrapper.find('FormGroup[label="Ansible Tower Hostname"]').length wrapper.find('FormGroup[label="Ansible Controller Hostname"]').length
).toBe(1); ).toBe(1);
expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Username"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);

View File

@@ -61,7 +61,7 @@
}, },
"created": "2020-05-18T21:53:35.334813Z", "created": "2020-05-18T21:53:35.334813Z",
"modified": "2020-05-18T21:54:05.424087Z", "modified": "2020-05-18T21:54:05.424087Z",
"name": "Ansible Tower", "name": "Ansible Controller",
"description": "", "description": "",
"kind": "cloud", "kind": "cloud",
"namespace": "tower", "namespace": "tower",
@@ -70,9 +70,9 @@
"fields": [ "fields": [
{ {
"id": "host", "id": "host",
"label": "Ansible Tower Hostname", "label": "Ansible Controller Hostname",
"type": "string", "type": "string",
"help_text": "The Ansible Tower base URL to authenticate with." "help_text": "The Ansible Controller base URL to authenticate with."
}, },
{ {
"id": "username", "id": "username",

View File

@@ -3,7 +3,7 @@
"type": "credential", "type": "credential",
"url": "/api/v2/credentials/4/", "url": "/api/v2/credentials/4/",
"related": { "related": {
"named_url": "/api/v2/credentials/Tower cred++Ansible Tower+cloud++/", "named_url": "/api/v2/credentials/Tower cred++Ansible Controller+cloud++/",
"created_by": "/api/v2/users/2/", "created_by": "/api/v2/users/2/",
"modified_by": "/api/v2/users/2/", "modified_by": "/api/v2/users/2/",
"activity_stream": "/api/v2/credentials/4/activity_stream/", "activity_stream": "/api/v2/credentials/4/activity_stream/",
@@ -19,7 +19,7 @@
"summary_fields": { "summary_fields": {
"credential_type": { "credential_type": {
"id": 16, "id": 16,
"name": "Ansible Tower", "name": "Ansible Controller",
"description": "" "description": ""
}, },
"created_by": { "created_by": {

View File

@@ -32,7 +32,7 @@ function CredentialTypeFormFields() {
/> />
<FormFullWidthLayout> <FormFullWidthLayout>
<VariablesField <VariablesField
tooltip={t`Enter inputs using either JSON or YAML syntax. Refer to the Ansible Tower documentation for example syntax.`} tooltip={t`Enter inputs using either JSON or YAML syntax. Refer to the Ansible Controller documentation for example syntax.`}
id="credential-type-inputs-configuration" id="credential-type-inputs-configuration"
name="inputs" name="inputs"
label={t`Input configuration`} label={t`Input configuration`}
@@ -40,7 +40,7 @@ function CredentialTypeFormFields() {
</FormFullWidthLayout> </FormFullWidthLayout>
<FormFullWidthLayout> <FormFullWidthLayout>
<VariablesField <VariablesField
tooltip={t`Enter injectors using either JSON or YAML syntax. Refer to the Ansible Tower documentation for example syntax.`} tooltip={t`Enter injectors using either JSON or YAML syntax. Refer to the Ansible Controller documentation for example syntax.`}
id="credential-type-injectors-configuration" id="credential-type-injectors-configuration"
name="injectors" name="injectors"
label={t`Injector configuration`} label={t`Injector configuration`}

View File

@@ -29,6 +29,7 @@ import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDeta
import useIsMounted from 'hooks/useIsMounted'; import useIsMounted from 'hooks/useIsMounted';
import { formatDateString } from 'util/dates'; import { formatDateString } from 'util/dates';
import Popover from 'components/Popover'; import Popover from 'components/Popover';
import { VERBOSITY } from 'components/VerbositySelectField';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails'; import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails';
import helpText from '../shared/Inventory.helptext'; import helpText from '../shared/Inventory.helptext';
@@ -111,12 +112,6 @@ function InventorySourceDetail({ inventorySource }) {
inventorySource.id inventorySource.id
); );
const VERBOSITY = {
0: t`0 (Warning)`,
1: t`1 (Info)`,
2: t`2 (Debug)`,
};
let optionsList = ''; let optionsList = '';
if ( if (
overwrite || overwrite ||
@@ -251,7 +246,7 @@ function InventorySourceDetail({ inventorySource }) {
<Detail <Detail
label={t`Verbosity`} label={t`Verbosity`}
helpText={helpText.subFormVerbosityFields} helpText={helpText.subFormVerbosityFields}
value={VERBOSITY[verbosity]} value={VERBOSITY()[verbosity]}
/> />
<Detail <Detail
label={t`Cache timeout`} label={t`Cache timeout`}

View File

@@ -93,7 +93,7 @@ describe('InventorySourceDetail', () => {
assertDetail(wrapper, 'Organization', 'Mock Org'); assertDetail(wrapper, 'Organization', 'Mock Org');
assertDetail(wrapper, 'Project', 'Mock Project'); assertDetail(wrapper, 'Project', 'Mock Project');
assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Inventory file', 'foo');
assertDetail(wrapper, 'Verbosity', '2 (Debug)'); assertDetail(wrapper, 'Verbosity', '2 (More Verbose)');
assertDetail(wrapper, 'Cache timeout', '2 seconds'); assertDetail(wrapper, 'Cache timeout', '2 seconds');
const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail');
expect(executionEnvironment).toHaveLength(1); expect(executionEnvironment).toHaveLength(1);

View File

@@ -93,7 +93,7 @@ const SmartInventoryFormFields = ({ inventory }) => {
label={t`Variables`} label={t`Variables`}
tooltip={t`Enter inventory variables using either JSON or YAML syntax. tooltip={t`Enter inventory variables using either JSON or YAML syntax.
Use the radio button to toggle between the two. Refer to the Use the radio button to toggle between the two. Refer to the
Ansible Tower documentation for example syntax.`} Ansible Controller documentation for example syntax.`}
/> />
</FormFullWidthLayout> </FormFullWidthLayout>
</> </>

View File

@@ -25,6 +25,7 @@ import { LaunchButton, ReLaunchDropDown } from 'components/LaunchButton';
import StatusLabel from 'components/StatusLabel'; import StatusLabel from 'components/StatusLabel';
import JobCancelButton from 'components/JobCancelButton'; import JobCancelButton from 'components/JobCancelButton';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail'; import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import { VERBOSITY } from 'components/VerbositySelectField';
import { getJobModel, isJobRunning } from 'util/jobs'; import { getJobModel, isJobRunning } from 'util/jobs';
import { formatDateString } from 'util/dates'; import { formatDateString } from 'util/dates';
import { Job } from 'types'; import { Job } from 'types';
@@ -37,14 +38,6 @@ const StatusDetailValue = styled.div`
grid-template-columns: auto auto; grid-template-columns: auto auto;
`; `;
const VERBOSITY = {
0: '0 (Normal)',
1: '1 (Verbose)',
2: '2 (More Verbose)',
3: '3 (Debug)',
4: '4 (Connection Debug)',
};
function JobDetail({ job, inventorySourceLabels }) { function JobDetail({ job, inventorySourceLabels }) {
const { me } = useConfig(); const { me } = useConfig();
const { const {
@@ -332,7 +325,7 @@ function JobDetail({ job, inventorySourceLabels }) {
dataCy="job-verbosity" dataCy="job-verbosity"
label={t`Verbosity`} label={t`Verbosity`}
helpText={jobHelpText.verbosity} helpText={jobHelpText.verbosity}
value={VERBOSITY[job.verbosity]} value={VERBOSITY()[job.verbosity]}
/> />
{job.type !== 'workflow_job' && !isJobRunning(job.status) && ( {job.type !== 'workflow_job' && !isJobRunning(job.status) && (
<ExecutionEnvironmentDetail <ExecutionEnvironmentDetail

View File

@@ -1,17 +1,29 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import 'styled-components/macro'; import 'styled-components/macro';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { SearchIcon } from '@patternfly/react-icons'; import {
SearchIcon,
ExclamationCircleIcon as PFExclamationCircleIcon,
} from '@patternfly/react-icons';
import ContentEmpty from 'components/ContentEmpty'; import ContentEmpty from 'components/ContentEmpty';
import styled from 'styled-components';
const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
color: var(--pf-global--danger-color--100);
`;
export default function EmptyOutput({ export default function EmptyOutput({
hasQueryParams, hasQueryParams,
isJobRunning, isJobRunning,
onUnmount, onUnmount,
job,
}) { }) {
let title; let title;
let message; let message;
let icon; let icon;
const { typeSegment, id } = useParams();
useEffect(() => onUnmount); useEffect(() => onUnmount);
@@ -21,6 +33,21 @@ export default function EmptyOutput({
icon = SearchIcon; icon = SearchIcon;
} else if (isJobRunning) { } else if (isJobRunning) {
title = t`Waiting for job output…`; title = t`Waiting for job output…`;
} else if (job.status === 'failed') {
title = t`This job failed and has no output.`;
message = (
<>
{t`Return to `}{' '}
<Link to={`/jobs/${typeSegment}/${id}/details`}>{t`details.`}</Link>
<br />
{job.job_explanation && (
<>
{t`Failure Explanation:`} {`${job.job_explanation}`}
</>
)}
</>
);
icon = ExclamationCircleIcon;
} else { } else {
title = t`No output found for this job.`; title = t`No output found for this job.`;
} }

View File

@@ -687,6 +687,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
) { ) {
return ( return (
<EmptyOutput <EmptyOutput
job={job}
hasQueryParams={location.search.length > 1} hasQueryParams={location.search.length > 1}
isJobRunning={isJobRunning(jobStatus)} isJobRunning={isJobRunning(jobStatus)}
onUnmount={() => { onUnmount={() => {

View File

@@ -134,4 +134,20 @@ describe('<JobOutput />', () => {
}); });
await waitForElement(wrapper, 'ContentError', (el) => el.length === 1); await waitForElement(wrapper, 'ContentError', (el) => el.length === 1);
}); });
test('should show failed empty output screen', async () => {
JobsAPI.readEvents.mockResolvedValue({
data: {
count: 0,
next: null,
previous: null,
results: [],
},
});
await act(async () => {
wrapper = mountWithContexts(
<JobOutput job={{ ...mockJob, status: 'failed' }} />
);
});
await waitForElement(wrapper, 'EmptyOutput', (el) => el.length === 1);
});
}); });

View File

@@ -93,7 +93,7 @@ function CustomMessagesSubForm({ defaultMessages, type }) {
config config
)}/html/userguide/notifications.html#create-custom-notifications`} )}/html/userguide/notifications.html#create-custom-notifications`}
> >
{t`Ansible Tower Documentation.`} {t`Ansible Controller Documentation.`}
</a> </a>
</small> </small>
</Text> </Text>

View File

@@ -28,7 +28,7 @@ const helpText = {
twilioDestinationNumbers: t`Use one phone number per line to specify where to twilioDestinationNumbers: t`Use one phone number per line to specify where to
route SMS messages. Phone numbers should be formatted +11231231234. For more information see Twilio documentation`, route SMS messages. Phone numbers should be formatted +11231231234. For more information see Twilio documentation`,
webhookHeaders: t`Specify HTTP Headers in JSON format. Refer to webhookHeaders: t`Specify HTTP Headers in JSON format. Refer to
the Ansible Tower documentation for example syntax.`, the Ansible Controller documentation for example syntax.`,
}; };
export default helpText; export default helpText;

View File

@@ -35,6 +35,13 @@ function SubscriptionDetail() {
}, },
]; ];
const { automated_instances: automatedInstancesCount, automated_since } =
license_info;
const automatedInstancesSinceDateTime = automated_since
? formatDateString(new Date(automated_since * 1000).toISOString())
: null;
return ( return (
<> <>
<RoutedTabs tabsArray={tabsArray} /> <RoutedTabs tabsArray={tabsArray} />
@@ -127,19 +134,23 @@ function SubscriptionDetail() {
label={t`Hosts imported`} label={t`Hosts imported`}
value={license_info.current_instances} value={license_info.current_instances}
/> />
{typeof automatedInstancesCount !== 'undefined' &&
automatedInstancesCount !== null && (
<Detail <Detail
dataCy="subscription-hosts-automated" dataCy="subscription-hosts-automated"
label={t`Hosts automated`} label={t`Hosts automated`}
value={ value={
<> automated_since ? (
{license_info.automated_instances} <Trans>since</Trans>{' '} <Trans>
{license_info.automated_since && {automatedInstancesCount} since{' '}
formatDateString( {automatedInstancesSinceDateTime}
new Date(license_info.automated_since * 1000).toISOString() </Trans>
)} ) : (
</> automatedInstancesCount
)
} }
/> />
)}
<Detail <Detail
dataCy="subscription-hosts-remaining" dataCy="subscription-hosts-remaining"
label={t`Hosts remaining`} label={t`Hosts remaining`}

View File

@@ -82,4 +82,17 @@ describe('<SubscriptionDetail />', () => {
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1); expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
}); });
test('should not render Hosts Automated Detail if license_info.automated_instances is undefined', () => {
wrapper = mountWithContexts(<SubscriptionDetail />, {
context: {
config: {
...config,
license_info: { ...config.license_info, automated_instances: null },
},
},
});
expect(wrapper.find(`Detail[label="Hosts automated"]`).length).toBe(0);
});
}); });

View File

@@ -28,6 +28,7 @@ import DeleteButton from 'components/DeleteButton';
import ErrorDetail from 'components/ErrorDetail'; import ErrorDetail from 'components/ErrorDetail';
import { LaunchButton } from 'components/LaunchButton'; import { LaunchButton } from 'components/LaunchButton';
import { VariablesDetail } from 'components/CodeEditor'; import { VariablesDetail } from 'components/CodeEditor';
import { VERBOSITY } from 'components/VerbositySelectField';
import { JobTemplatesAPI } from 'api'; import { JobTemplatesAPI } from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest'; import useRequest, { useDismissableError } from 'hooks/useRequest';
import useBrandName from 'hooks/useBrandName'; import useBrandName from 'hooks/useBrandName';
@@ -104,17 +105,6 @@ function JobTemplateDetail({ template }) {
relatedResourceDeleteRequests.template(template); relatedResourceDeleteRequests.template(template);
const canLaunch = const canLaunch =
summary_fields.user_capabilities && summary_fields.user_capabilities.start; summary_fields.user_capabilities && summary_fields.user_capabilities.start;
const verbosityOptions = [
{ verbosity: 0, details: t`0 (Normal)` },
{ verbosity: 1, details: t`1 (Verbose)` },
{ verbosity: 2, details: t`2 (More Verbose)` },
{ verbosity: 3, details: t`3 (Debug)` },
{ verbosity: 4, details: t`4 (Connection Debug)` },
{ verbosity: 5, details: t`5 (WinRM Debug)` },
];
const verbosityDetails = verbosityOptions.filter(
(option) => option.verbosity === verbosity
);
const generateCallBackUrl = `${window.location.origin + url}callback/`; const generateCallBackUrl = `${window.location.origin + url}callback/`;
const renderOptionsField = const renderOptionsField =
become_enabled || become_enabled ||
@@ -272,7 +262,7 @@ function JobTemplateDetail({ template }) {
/> />
<Detail <Detail
label={t`Verbosity`} label={t`Verbosity`}
value={verbosityDetails[0].details} value={VERBOSITY()[verbosity]}
dataCy="jt-detail-verbosity" dataCy="jt-detail-verbosity"
helpText={helpText.verbosity} helpText={helpText.verbosity}
/> />

View File

@@ -44,7 +44,7 @@ function AnswerTypeField() {
labelIcon={ labelIcon={
<Popover <Popover
content={t`Choose an answer type or format you want as the prompt for the user. content={t`Choose an answer type or format you want as the prompt for the user.
Refer to the Ansible Tower Documentation for more additional Refer to the Ansible Controller Documentation for more additional
information about each option.`} information about each option.`}
/> />
} }
@@ -266,8 +266,8 @@ function SurveyQuestionForm({
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
{t`documentation`}{' '} {t`documentation`}
</a> </a>{' '}
{t`for more information.`} {t`for more information.`}
</> </>
} }

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import getDocsBaseUrl from 'util/getDocsBaseUrl';
const jtHelpTextStrings = { const jtHelpTextStrings = {
jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`, jobType: t`For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.`,
@@ -46,6 +47,19 @@ const jtHelpTextStrings = {
{t`Refer to the Ansible documentation for details about the configuration file.`} {t`Refer to the Ansible documentation for details about the configuration file.`}
</span> </span>
), ),
localTimeZone: (config = '') => (
<span>
{t`Refer to the`}{' '}
<a
href={`${getDocsBaseUrl(config)}/html/userguide/scheduling.html`}
target="_blank"
rel="noreferrer"
>
{t`documentation`}
</a>{' '}
{t`for more information.`}
</span>
),
}; };
export default jtHelpTextStrings; export default jtHelpTextStrings;

View File

@@ -43,6 +43,7 @@ import Popover from 'components/Popover';
import { JobTemplatesAPI } from 'api'; import { JobTemplatesAPI } from 'api';
import useIsMounted from 'hooks/useIsMounted'; import useIsMounted from 'hooks/useIsMounted';
import LabelSelect from 'components/LabelSelect'; import LabelSelect from 'components/LabelSelect';
import { VerbositySelectField } from 'components/VerbositySelectField';
import PlaybookSelect from './PlaybookSelect'; import PlaybookSelect from './PlaybookSelect';
import WebhookSubForm from './WebhookSubForm'; import WebhookSubForm from './WebhookSubForm';
import helpText from './JobTemplate.helptext'; import helpText from './JobTemplate.helptext';
@@ -85,7 +86,6 @@ function JobTemplateForm({
const [credentialField, , credentialHelpers] = useField('credentials'); const [credentialField, , credentialHelpers] = useField('credentials');
const [labelsField, , labelsHelpers] = useField('labels'); const [labelsField, , labelsHelpers] = useField('labels');
const [limitField, limitMeta, limitHelpers] = useField('limit'); const [limitField, limitMeta, limitHelpers] = useField('limit');
const [verbosityField] = useField('verbosity');
const [diffModeField, , diffModeHelpers] = useField('diff_mode'); const [diffModeField, , diffModeHelpers] = useField('diff_mode');
const [instanceGroupsField, , instanceGroupsHelpers] = const [instanceGroupsField, , instanceGroupsHelpers] =
useField('instanceGroups'); useField('instanceGroups');
@@ -215,13 +215,6 @@ function JobTemplateForm({
isDisabled: false, isDisabled: false,
}, },
]; ];
const verbosityOptions = [
{ value: '0', key: '0', label: t`0 (Normal)` },
{ value: '1', key: '1', label: t`1 (Verbose)` },
{ value: '2', key: '2', label: t`2 (More Verbose)` },
{ value: '3', key: '3', label: t`3 (Debug)` },
{ value: '4', key: '4', label: t`4 (Connection Debug)` },
];
let callbackUrl; let callbackUrl;
if (template?.related) { if (template?.related) {
const path = template.related.callback || `${template.url}callback`; const path = template.related.callback || `${template.url}callback`;
@@ -428,19 +421,12 @@ function JobTemplateForm({
}} }}
/> />
</FieldWithPrompt> </FieldWithPrompt>
<FieldWithPrompt <VerbositySelectField
fieldId="template-verbosity" fieldId="template-verbosity"
label={t`Verbosity`}
promptId="template-ask-verbosity-on-launch" promptId="template-ask-verbosity-on-launch"
promptName="ask_verbosity_on_launch" promptName="ask_verbosity_on_launch"
tooltip={helpText.verbosity} tooltip={helpText.verbosity}
>
<AnsibleSelect
id="template-verbosity"
data={verbosityOptions}
{...verbosityField}
/> />
</FieldWithPrompt>
<FormField <FormField
id="template-job-slicing" id="template-job-slicing"
name="job_slice_count" name="job_slice_count"

View File

@@ -11,7 +11,7 @@ const wfHelpTextStrings = {
labels: t`Optional labels that describe this job template, labels: t`Optional labels that describe this job template,
such as 'dev' or 'test'. Labels can be used to group and filter such as 'dev' or 'test'. Labels can be used to group and filter
job templates and completed jobs.`, job templates and completed jobs.`,
variables: t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax.`, variables: t`Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Controller documentation for example syntax.`,
enableWebhook: t`Enable Webhook for this workflow job template.`, enableWebhook: t`Enable Webhook for this workflow job template.`,
enableConcurrentJobs: t`If enabled, simultaneous runs of this workflow job template will be allowed.`, enableConcurrentJobs: t`If enabled, simultaneous runs of this workflow job template will be allowed.`,
webhookURL: t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.`, webhookURL: t`Webhook services can launch jobs with this workflow job template by making a POST request to this URL.`,

View File

@@ -105,9 +105,6 @@ options:
description: description:
- Project to use as source with scm option - Project to use as source with scm option
type: str type: str
update_on_project_update:
description: Update this source when the related project updates if source is C(scm)
type: bool
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
@@ -181,7 +178,6 @@ def main():
update_on_launch=dict(type='bool'), update_on_launch=dict(type='bool'),
update_cache_timeout=dict(type='int'), update_cache_timeout=dict(type='int'),
source_project=dict(), source_project=dict(),
update_on_project_update=dict(type='bool'),
notification_templates_started=dict(type="list", elements='str'), notification_templates_started=dict(type="list", elements='str'),
notification_templates_success=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'),
notification_templates_error=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'),
@@ -273,7 +269,6 @@ def main():
'verbosity', 'verbosity',
'update_on_launch', 'update_on_launch',
'update_cache_timeout', 'update_cache_timeout',
'update_on_project_update',
'enabled_var', 'enabled_var',
'enabled_value', 'enabled_value',
'host_filter', 'host_filter',

View File

@@ -105,7 +105,7 @@ options:
- 5 - 5
unified_job_template: unified_job_template:
description: description:
- Name of unified job template to schedule. - Name of unified job template to schedule. Used to look up an already existing schedule.
required: False required: False
type: str type: str
organization: organization:
@@ -158,6 +158,12 @@ EXAMPLES = '''
every: 1 every: 1
on_days: 'sunday' on_days: 'sunday'
include: False include: False
- name: Delete 'my_schedule' schedule for my_workflow
schedule:
name: "my_schedule"
state: absent
unified_job_template: my_workflow
''' '''
from ..module_utils.controller_api import ControllerAPIModule from ..module_utils.controller_api import ControllerAPIModule
@@ -214,14 +220,16 @@ def main():
if inventory: if inventory:
inventory_id = module.resolve_name_to_id('inventories', inventory) inventory_id = module.resolve_name_to_id('inventories', inventory)
search_fields = {} search_fields = {}
sched_search_fields = {}
if organization: if organization:
search_fields['organization'] = module.resolve_name_to_id('organizations', organization) search_fields['organization'] = module.resolve_name_to_id('organizations', organization)
unified_job_template_id = None unified_job_template_id = None
if unified_job_template: if unified_job_template:
search_fields['name'] = unified_job_template search_fields['name'] = unified_job_template
unified_job_template_id = module.get_one('unified_job_templates', **{'data': search_fields})['id'] unified_job_template_id = module.get_one('unified_job_templates', **{'data': search_fields})['id']
sched_search_fields['unified_job_template'] = unified_job_template_id
# Attempt to look up an existing item based on the provided data # Attempt to look up an existing item based on the provided data
existing_item = module.get_one('schedules', name_or_id=name) existing_item = module.get_one('schedules', name_or_id=name, **{'data': sched_search_fields})
association_fields = {} association_fields = {}

View File

@@ -736,7 +736,7 @@ def main():
webhook_credential = module.params.get('webhook_credential') webhook_credential = module.params.get('webhook_credential')
if webhook_credential: if webhook_credential:
new_fields['webhook_credential'] = module.resolve_name_to_id('webhook_credential', webhook_credential) new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential)
# Create the data that gets sent for create and update # Create the data that gets sent for create and update
new_fields['name'] = new_name if new_name else (module.get_item_name(existing_item) if existing_item else name) new_fields['name'] = new_name if new_name else (module.get_item_name(existing_item) if existing_item else name)

View File

@@ -6,7 +6,7 @@ import pytest
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from awx.main.models import Schedule from awx.main.models import JobTemplate, Schedule
from awx.api.serializers import SchedulePreviewSerializer from awx.api.serializers import SchedulePreviewSerializer
@@ -24,6 +24,19 @@ def test_create_schedule(run_module, job_template, admin_user):
assert schedule.rrule == my_rrule assert schedule.rrule == my_rrule
@pytest.mark.django_db
def test_delete_same_named_schedule(run_module, project, inventory, admin_user):
jt1 = JobTemplate.objects.create(name='jt1', project=project, inventory=inventory, playbook='helloworld.yml')
jt2 = JobTemplate.objects.create(name='jt2', project=project, inventory=inventory, playbook='helloworld2.yml')
Schedule.objects.create(name='Some Schedule', rrule='DTSTART:20300112T210000Z RRULE:FREQ=DAILY;INTERVAL=1', unified_job_template=jt1)
Schedule.objects.create(name='Some Schedule', rrule='DTSTART:20300112T210000Z RRULE:FREQ=DAILY;INTERVAL=1', unified_job_template=jt2)
result = run_module('schedule', {'name': 'Some Schedule', 'unified_job_template': 'jt1', 'state': 'absent'}, admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert Schedule.objects.filter(name='Some Schedule').count() == 1
@pytest.mark.parametrize( @pytest.mark.parametrize(
"freq, kwargs, expect", "freq, kwargs, expect",
[ [

View File

@@ -163,6 +163,7 @@
- name: Disable a schedule - name: Disable a schedule
schedule: schedule:
name: "{{ sched1 }}" name: "{{ sched1 }}"
unified_job_template: "{{ jt1 }}"
state: present state: present
enabled: "false" enabled: "false"
register: result register: result
@@ -188,6 +189,29 @@
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result register: result
- name: Verify we can't find the schedule without the UJT lookup
schedule:
name: "{{ sched1 }}"
state: present
rrule: "DTSTART:20201219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
ignore_errors: true
- assert:
that:
- result is failed
- name: Verify we can find the schedule with the UJT lookup and delete it
schedule:
name: "{{ sched1 }}"
state: absent
unified_job_template: "{{ jt2 }}"
register: result
- assert:
that:
- result is changed
always: always:
- name: Delete the schedule - name: Delete the schedule
schedule: schedule:

View File

@@ -14,30 +14,38 @@
approval_node_name: "AWX-Collection-tests-workflow_approval_node-{{ test_id }}" approval_node_name: "AWX-Collection-tests-workflow_approval_node-{{ test_id }}"
lab1: "AWX-Collection-tests-job_template-lab1-{{ test_id }}" lab1: "AWX-Collection-tests-job_template-lab1-{{ test_id }}"
wfjt_name: "AWX-Collection-tests-workflow_job_template-wfjt-{{ test_id }}" wfjt_name: "AWX-Collection-tests-workflow_job_template-wfjt-{{ test_id }}"
webhook_wfjt_name: "AWX-Collection-tests-workflow_job_template-webhook-wfjt-{{ test_id }}"
email_not: "AWX-Collection-tests-job_template-email-not-{{ test_id }}" email_not: "AWX-Collection-tests-job_template-email-not-{{ test_id }}"
webhook_not: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}" webhook_notification: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}"
project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
github_webhook_credential_name: "AWX-Collection-tests-credential-webhook-{{ test_id }}_github"
- name: "Create a new organization" - block:
- name: "Create a new organization"
organization: organization:
name: "{{ org_name }}" name: "{{ org_name }}"
galaxy_credentials: galaxy_credentials:
- Ansible Galaxy - Ansible Galaxy
register: result register: result
- name: Create an SCM Credential - name: Create Credentials
credential: credential:
name: "{{ scm_cred_name }}" name: "{{ item.name }}"
organization: Default organization: Default
credential_type: Source Control credential_type: "{{ item.type }}"
register: result register: result
loop:
- name: "{{ scm_cred_name }}"
type: Source Control
- name: "{{ github_webhook_credential_name }}"
type: GitHub Personal Access Token
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Add email notification - name: Add email notification
notification_template: notification_template:
name: "{{ email_not }}" name: "{{ email_not }}"
organization: Default organization: Default
@@ -54,9 +62,9 @@
use_ssl: false use_ssl: false
state: present state: present
- name: Add webhook notification - name: Add webhook notification
notification_template: notification_template:
name: "{{ webhook_not }}" name: "{{ webhook_notification }}"
organization: Default organization: Default
notification_type: webhook notification_type: webhook
notification_configuration: notification_configuration:
@@ -66,7 +74,7 @@
state: present state: present
register: result register: result
- name: Create Labels - name: Create Labels
label: label:
name: "{{ lab1 }}" name: "{{ lab1 }}"
organization: "{{ item }}" organization: "{{ item }}"
@@ -74,7 +82,7 @@
- Default - Default
- "{{ org_name }}" - "{{ org_name }}"
- name: Create a Demo Project - name: Create a Demo Project
project: project:
name: "{{ demo_project_name }}" name: "{{ demo_project_name }}"
organization: Default organization: Default
@@ -84,11 +92,11 @@
scm_credential: "{{ scm_cred_name }}" scm_credential: "{{ scm_cred_name }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Create a 2nd Demo Project in another org - name: Create a 2nd Demo Project in another org
project: project:
name: "{{ demo_project_name_2 }}" name: "{{ demo_project_name_2 }}"
organization: "{{ org_name }}" organization: "{{ org_name }}"
@@ -98,17 +106,17 @@
scm_credential: "{{ scm_cred_name }}" scm_credential: "{{ scm_cred_name }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Add an inventory - name: Add an inventory
inventory: inventory:
description: Test inventory description: Test inventory
organization: Default organization: Default
name: "{{ project_inv }}" name: "{{ project_inv }}"
- name: Create a source inventory - name: Create a source inventory
inventory_source: inventory_source:
name: "{{ project_inv_source }}" name: "{{ project_inv_source }}"
description: Source for Test inventory description: Source for Test inventory
@@ -119,11 +127,11 @@
source: scm source: scm
register: project_inv_source_result register: project_inv_source_result
- assert: - assert:
that: that:
- "project_inv_source_result is changed" - "project_inv_source_result is changed"
- name: Create a Job Template - name: Create a Job Template
job_template: job_template:
name: "{{ jt1_name }}" name: "{{ jt1_name }}"
project: "{{ demo_project_name }}" project: "{{ demo_project_name }}"
@@ -133,11 +141,11 @@
state: present state: present
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Create a second Job Template - name: Create a second Job Template
job_template: job_template:
name: "{{ jt2_name }}" name: "{{ jt2_name }}"
project: "{{ demo_project_name }}" project: "{{ demo_project_name }}"
@@ -147,11 +155,11 @@
state: present state: present
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Create a second Job Template in new org - name: Create a second Job Template in new org
job_template: job_template:
name: "{{ jt2_name }}" name: "{{ jt2_name }}"
project: "{{ demo_project_name_2 }}" project: "{{ demo_project_name_2 }}"
@@ -161,11 +169,11 @@
state: present state: present
register: jt2_name_result register: jt2_name_result
- assert: - assert:
that: that:
- "jt2_name_result is changed" - "jt2_name_result is changed"
- name: Add a Survey to second Job Template - name: Add a Survey to second Job Template
job_template: job_template:
name: "{{ jt2_name }}" name: "{{ jt2_name }}"
organization: Default organization: Default
@@ -178,11 +186,11 @@
survey_spec: '{"spec": [{"index": 0, "question_name": "my question?", "default": "mydef", "variable": "myvar", "type": "text", "required": false}], "description": "test", "name": "test"}' survey_spec: '{"spec": [{"index": 0, "question_name": "my question?", "default": "mydef", "variable": "myvar", "type": "text", "required": false}], "description": "test", "name": "test"}'
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Create a workflow job template - name: Create a workflow job template
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
organization: Default organization: Default
@@ -196,11 +204,11 @@
ask_variables_on_launch: true ask_variables_on_launch: true
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Create a workflow job template with bad label - name: Create a workflow job template with bad label
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
organization: Default organization: Default
@@ -215,12 +223,12 @@
register: bad_label_results register: bad_label_results
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "bad_label_results.msg == 'Could not find label entry with name label_bad'" - "bad_label_results.msg == 'Could not find label entry with name label_bad'"
# Turn off ask_ * settings to test that the issue/10057 has been fixed # Turn off ask_ * settings to test that the issue/10057 has been fixed
- name: Turn ask_* settings OFF - name: Turn ask_* settings OFF
tower_workflow_job_template: tower_workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
ask_inventory_on_launch: false ask_inventory_on_launch: false
@@ -229,25 +237,25 @@
ask_variables_on_launch: false ask_variables_on_launch: false
state: present state: present
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
# Node actions do what the schema command used to do # Node actions do what the schema command used to do
- name: Create leaf node - name: Create leaf node
workflow_job_template_node: workflow_job_template_node:
identifier: leaf identifier: leaf
unified_job_template: "{{ jt2_name }}" unified_job_template: "{{ jt2_name }}"
lookup_organization: "{{ org_name }}" lookup_organization: "{{ org_name }}"
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
- name: Create root node - name: Create root node
workflow_job_template_node: workflow_job_template_node:
identifier: root identifier: root
unified_job_template: "{{ jt1_name }}" unified_job_template: "{{ jt1_name }}"
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
- name: Fail if no name is set for approval - name: Fail if no name is set for approval
workflow_job_template_node: workflow_job_template_node:
identifier: approval_test identifier: approval_test
approval_node: approval_node:
@@ -256,11 +264,11 @@
register: no_name_results register: no_name_results
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "no_name_results.msg == 'Approval node name is required to create approval node.'" - "no_name_results.msg == 'Approval node name is required to create approval node.'"
- name: Fail if absent and no identifier set - name: Fail if absent and no identifier set
workflow_job_template_node: workflow_job_template_node:
approval_node: approval_node:
description: "{{ approval_node_name }}" description: "{{ approval_node_name }}"
@@ -269,22 +277,22 @@
register: no_identifier_results register: no_identifier_results
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "no_identifier_results.msg == 'missing required arguments: identifier'" - "no_identifier_results.msg == 'missing required arguments: identifier'"
- name: Fail if present and no unified job template set - name: Fail if present and no unified job template set
workflow_job_template_node: workflow_job_template_node:
identifier: approval_test identifier: approval_test
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
register: no_unified_results register: no_unified_results
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "no_unified_results.msg == 'state is present but any of the following are missing: unified_job_template, approval_node, success_nodes, always_nodes, failure_nodes'" - "no_unified_results.msg == 'state is present but any of the following are missing: unified_job_template, approval_node, success_nodes, always_nodes, failure_nodes'"
- name: Create approval node - name: Create approval node
workflow_job_template_node: workflow_job_template_node:
identifier: approval_test identifier: approval_test
approval_node: approval_node:
@@ -292,7 +300,7 @@
timeout: 900 timeout: 900
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
- name: Create link for root node - name: Create link for root node
workflow_job_template_node: workflow_job_template_node:
identifier: root identifier: root
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
@@ -301,7 +309,7 @@
always_nodes: always_nodes:
- leaf - leaf
- name: Delete approval node - name: Delete approval node
workflow_job_template_node: workflow_job_template_node:
identifier: approval_test identifier: approval_test
approval_node: approval_node:
@@ -309,54 +317,54 @@
state: absent state: absent
workflow: "{{ wfjt_name }}" workflow: "{{ wfjt_name }}"
- name: Add started notifications to workflow job template - name: Add started notifications to workflow job template
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
notification_templates_started: notification_templates_started:
- "{{ email_not }}" - "{{ email_not }}"
- "{{ webhook_not }}" - "{{ webhook_notification }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Re Add started notifications to workflow job template - name: Re Add started notifications to workflow job template
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
notification_templates_started: notification_templates_started:
- "{{ email_not }}" - "{{ email_not }}"
- "{{ webhook_not }}" - "{{ webhook_notification }}"
register: result register: result
- assert: - assert:
that: that:
- "result is not changed" - "result is not changed"
- name: Add success notifications to workflow job template - name: Add success notifications to workflow job template
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
notification_templates_success: notification_templates_success:
- "{{ email_not }}" - "{{ email_not }}"
- "{{ webhook_not }}" - "{{ webhook_notification }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Copy a workflow job template - name: Copy a workflow job template
workflow_job_template: workflow_job_template:
name: "copy_{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
copy_from: "{{ wfjt_name }}" copy_from: "{{ wfjt_name }}"
organization: Default organization: Default
register: result register: result
- assert: - assert:
that: that:
- result.copied - result.copied
- name: Fail Remove "on start" webhook notification from copied workflow job template - name: Fail Remove "on start" webhook notification from copied workflow job template
workflow_job_template: workflow_job_template:
name: "copy_{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
notification_templates_started: notification_templates_started:
@@ -364,24 +372,24 @@
register: remove_copied_workflow_node register: remove_copied_workflow_node
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "remove_copied_workflow_node is failed" - "remove_copied_workflow_node is failed"
- "remove_copied_workflow_node is not changed" - "remove_copied_workflow_node is not changed"
- "'returned 0 items' in remove_copied_workflow_node.msg" - "'returned 0 items' in remove_copied_workflow_node.msg"
- name: Remove "on start" webhook notification from copied workflow job template - name: Remove "on start" webhook notification from copied workflow job template
workflow_job_template: workflow_job_template:
name: "copy_{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
notification_templates_started: notification_templates_started:
- "{{ email_not }}" - "{{ email_not }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Add Survey to Copied workflow job template - name: Add Survey to Copied workflow job template
workflow_job_template: workflow_job_template:
name: "copy_{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
organization: Default organization: Default
@@ -436,11 +444,11 @@
new_question: true new_question: true
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Re add survey to workflow job template expected not changed. - name: Re add survey to workflow job template expected not changed.
workflow_job_template: workflow_job_template:
name: "copy_{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
organization: Default organization: Default
@@ -495,32 +503,22 @@
new_question: true new_question: true
register: result register: result
- assert: - assert:
that: that:
- "result is not changed" - "result is not changed"
- name: Delete copied workflow job template - name: Remove "on start" webhook notification from workflow job template
workflow_job_template:
name: "copy_{{ wfjt_name }}"
state: absent
register: result
- assert:
that:
- "result is changed"
- name: Remove "on start" webhook notification from workflow job template
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
notification_templates_started: notification_templates_started:
- "{{ email_not }}" - "{{ email_not }}"
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Delete a workflow job template with an invalid inventory and webook_credential - name: Delete a workflow job template with an invalid inventory and webook_credential
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
inventory: "Does Not Exist" inventory: "Does Not Exist"
@@ -528,25 +526,25 @@
state: absent state: absent
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Check module fails with correct msg - name: Check module fails with correct msg
workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
organization: Non_Existing_Organization organization: Non_Existing_Organization
register: result register: result
ignore_errors: true ignore_errors: true
- assert: - assert:
that: that:
- "result is failed" - "result is failed"
- "result is not changed" - "result is not changed"
- "'Non_Existing_Organization' in result.msg" - "'Non_Existing_Organization' in result.msg"
- "result.total_results == 0" - "result.total_results == 0"
- name: Create a workflow job template with workflow nodes in template - name: Create a workflow job template with workflow nodes in template
awx.awx.workflow_job_template: awx.awx.workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
inventory: Demo Inventory inventory: Demo Inventory
@@ -591,22 +589,22 @@
type: system_job type: system_job
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - "result is changed"
- name: Kick off a workflow and wait for it - name: Kick off a workflow and wait for it
workflow_launch: workflow_launch:
workflow_template: "{{ wfjt_name }}" workflow_template: "{{ wfjt_name }}"
ignore_errors: true ignore_errors: true
register: result register: result
- assert: - assert:
that: that:
- result is not failed - result is not failed
- "'id' in result['job_info']" - "'id' in result['job_info']"
- name: Destroy previous workflow nodes for one that fails - name: Destroy previous workflow nodes for one that fails
awx.awx.workflow_job_template: awx.awx.workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ wfjt_name }}"
destroy_current_nodes: true destroy_current_nodes: true
@@ -630,33 +628,72 @@
type: inventory_source type: inventory_source
- identifier: Workflow inception - identifier: Workflow inception
unified_job_template: unified_job_template:
name: "{{ wfjt_name }}" name: "copy_{{ wfjt_name }}"
organization: organization:
name: Default name: Default
type: workflow_job_template type: workflow_job_template
register: result register: result
- name: Kick off a workflow and wait for it - name: Delete copied workflow job template
workflow_job_template:
name: "copy_{{ wfjt_name }}"
state: absent
register: result
- assert:
that:
- "result is changed"
- name: Kick off a workflow and wait for it
workflow_launch: workflow_launch:
workflow_template: "{{ wfjt_name }}" workflow_template: "{{ wfjt_name }}"
ignore_errors: true ignore_errors: true
register: result register: result
- assert: - assert:
that: that:
- result is failed - result is failed
- name: Delete a workflow job template - name: Create a workflow job template with a GitLab webhook but a GitHub credential
awx.awx.workflow_job_template: workflow_job_template:
name: "{{ wfjt_name }}" name: "{{ webhook_wfjt_name }}"
state: absent organization: Default
inventory: Demo Inventory
webhook_service: gitlab
webhook_credential: "{{ github_webhook_credential_name }}"
ignore_errors: true
register: result register: result
- assert: - assert:
that: that:
- "result is changed" - result is failed
- "'Must match the selected webhook service' in result['msg']"
- name: Delete the Job Template - name: Create a workflow job template with a GitHub webhook and a GitHub credential
workflow_job_template:
name: "{{ webhook_wfjt_name }}"
organization: Default
inventory: Demo Inventory
webhook_service: github
webhook_credential: "{{ github_webhook_credential_name }}"
register: result
- assert:
that:
- result is not failed
always:
- name: Delete the workflow job template
awx.awx.workflow_job_template:
name: "{{ item }}"
state: absent
ignore_errors: True
loop:
- "copy_{{ wfjt_name }}"
- "{{ wfjt_name }}"
- "{{ webhook_wfjt_name }}"
- name: Delete the Job Template
job_template: job_template:
name: "{{ jt1_name }}" name: "{{ jt1_name }}"
project: "{{ demo_project_name }}" project: "{{ demo_project_name }}"
@@ -664,13 +701,9 @@
playbook: hello_world.yml playbook: hello_world.yml
job_type: run job_type: run
state: absent state: absent
register: result ignore_errors: True
- assert: - name: Delete the second Job Template
that:
- "result is changed"
- name: Delete the second Job Template
job_template: job_template:
name: "{{ jt2_name }}" name: "{{ jt2_name }}"
project: "{{ demo_project_name }}" project: "{{ demo_project_name }}"
@@ -679,13 +712,9 @@
playbook: hello_world.yml playbook: hello_world.yml
job_type: run job_type: run
state: absent state: absent
register: result ignore_errors: True
- assert: - name: Delete the second Job Template
that:
- "result is changed"
- name: Delete the second Job Template
job_template: job_template:
name: "{{ jt2_name }}" name: "{{ jt2_name }}"
project: "{{ demo_project_name_2 }}" project: "{{ demo_project_name_2 }}"
@@ -694,35 +723,25 @@
playbook: hello_world.yml playbook: hello_world.yml
job_type: run job_type: run
state: absent state: absent
register: result ignore_errors: True
- assert: - name: Delete the inventory source
that:
- "result is changed"
- name: Delete the inventory source
inventory_source: inventory_source:
name: "{{ project_inv_source }}" name: "{{ project_inv_source }}"
inventory: "{{ project_inv }}" inventory: "{{ project_inv }}"
source: scm source: scm
state: absent state: absent
ignore_errors: True
- assert: - name: Delete the inventory
that:
- "result is changed"
- name: Delete the inventory
inventory: inventory:
description: Test inventory description: Test inventory
organization: Default organization: Default
name: "{{ project_inv }}" name: "{{ project_inv }}"
state: absent state: absent
ignore_errors: True
- assert: - name: Delete the Demo Project
that:
- "result is changed"
- name: Delete the Demo Project
project: project:
name: "{{ demo_project_name }}" name: "{{ demo_project_name }}"
organization: Default organization: Default
@@ -730,9 +749,9 @@
scm_url: https://github.com/ansible/ansible-tower-samples.git scm_url: https://github.com/ansible/ansible-tower-samples.git
scm_credential: "{{ scm_cred_name }}" scm_credential: "{{ scm_cred_name }}"
state: absent state: absent
register: result ignore_errors: True
- name: Delete the 2nd Demo Project - name: Delete the 2nd Demo Project
project: project:
name: "{{ demo_project_name_2 }}" name: "{{ demo_project_name_2 }}"
organization: "{{ org_name }}" organization: "{{ org_name }}"
@@ -740,38 +759,40 @@
scm_url: https://github.com/ansible/ansible-tower-samples.git scm_url: https://github.com/ansible/ansible-tower-samples.git
scm_credential: "{{ scm_cred_name }}" scm_credential: "{{ scm_cred_name }}"
state: absent state: absent
register: result ignore_errors: True
- assert: - name: Delete the SCM Credential
that:
- "result is changed"
- name: Delete the SCM Credential
credential: credential:
name: "{{ scm_cred_name }}" name: "{{ scm_cred_name }}"
organization: Default organization: Default
credential_type: Source Control credential_type: Source Control
state: absent state: absent
register: result ignore_errors: True
- assert: - name: Delete the GitHub Webhook Credential
that: credential:
- "result is changed" name: "{{ github_webhook_credential_name }}"
organization: Default
credential_type: GitHub Personal Access Token
state: absent
ignore_errors: True
- name: Delete email notification - name: Delete email notification
notification_template: notification_template:
name: "{{ email_not }}" name: "{{ email_not }}"
organization: Default organization: Default
state: absent state: absent
ignore_errors: True
- name: Delete webhook notification - name: Delete webhook notification
notification_template: notification_template:
name: "{{ webhook_not }}" name: "{{ webhook_notification }}"
organization: Default organization: Default
state: absent state: absent
ignore_errors: True
- name: "Remove the organization" - name: "Remove the organization"
organization: organization:
name: "{{ org_name }}" name: "{{ org_name }}"
state: absent state: absent
register: result ignore_errors: True

View File

@@ -79,6 +79,7 @@ class ApiV2(base.Base):
return None return None
if post_fields is None: # Deprecated endpoint or insufficient permissions if post_fields is None: # Deprecated endpoint or insufficient permissions
log.error("Object export failed: %s", _page.endpoint) log.error("Object export failed: %s", _page.endpoint)
self._has_error = True
return None return None
# Note: doing _page[key] automatically parses json blob strings, which can be a problem. # Note: doing _page[key] automatically parses json blob strings, which can be a problem.
@@ -99,6 +100,7 @@ class ApiV2(base.Base):
pass pass
if resource is None: if resource is None:
log.error("Unable to infer endpoint for %r on %s.", key, _page.endpoint) log.error("Unable to infer endpoint for %r on %s.", key, _page.endpoint)
self._has_error = True
continue continue
related = self._filtered_list(resource, _page.json[key]).results[0] related = self._filtered_list(resource, _page.json[key]).results[0]
else: else:
@@ -108,12 +110,14 @@ class ApiV2(base.Base):
if rel_endpoint is None: # This foreign key is unreadable if rel_endpoint is None: # This foreign key is unreadable
if post_fields[key].get('required'): if post_fields[key].get('required'):
log.error("Foreign key %r export failed for object %s.", key, _page.endpoint) log.error("Foreign key %r export failed for object %s.", key, _page.endpoint)
self._has_error = True
return None return None
log.warning("Foreign key %r export failed for object %s, setting to null", key, _page.endpoint) log.warning("Foreign key %r export failed for object %s, setting to null", key, _page.endpoint)
continue continue
rel_natural_key = rel_endpoint.get_natural_key(self._cache) rel_natural_key = rel_endpoint.get_natural_key(self._cache)
if rel_natural_key is None: if rel_natural_key is None:
log.error("Unable to construct a natural key for foreign key %r of object %s.", key, _page.endpoint) log.error("Unable to construct a natural key for foreign key %r of object %s.", key, _page.endpoint)
self._has_error = True
return None # This foreign key has unresolvable dependencies return None # This foreign key has unresolvable dependencies
fields[key] = rel_natural_key fields[key] = rel_natural_key
@@ -158,6 +162,7 @@ class ApiV2(base.Base):
natural_key = _page.get_natural_key(self._cache) natural_key = _page.get_natural_key(self._cache)
if natural_key is None: if natural_key is None:
log.error("Unable to construct a natural key for object %s.", _page.endpoint) log.error("Unable to construct a natural key for object %s.", _page.endpoint)
self._has_error = True
return None return None
fields['natural_key'] = natural_key fields['natural_key'] = natural_key
@@ -249,6 +254,7 @@ class ApiV2(base.Base):
except (exc.Common, AssertionError) as e: except (exc.Common, AssertionError) as e:
identifier = asset.get("name", None) or asset.get("username", None) or asset.get("hostname", None) identifier = asset.get("name", None) or asset.get("username", None) or asset.get("hostname", None)
log.error(f"{endpoint} \"{identifier}\": {e}.") log.error(f"{endpoint} \"{identifier}\": {e}.")
self._has_error = True
log.debug("post_data: %r", post_data) log.debug("post_data: %r", post_data)
continue continue
@@ -283,6 +289,7 @@ class ApiV2(base.Base):
pass pass
except exc.Common as e: except exc.Common as e:
log.error("Role assignment failed: %s.", e) log.error("Role assignment failed: %s.", e)
self._has_error = True
log.debug("post_data: %r", {'id': role_page['id']}) log.debug("post_data: %r", {'id': role_page['id']})
def _assign_membership(self): def _assign_membership(self):
@@ -313,17 +320,21 @@ class ApiV2(base.Base):
for item in related_set: for item in related_set:
rel_page = self._cache.get_by_natural_key(item) rel_page = self._cache.get_by_natural_key(item)
if rel_page is None: if rel_page is None:
continue # FIXME log.error("Could not find matching object in Tower for imported relation, item: %r", item)
self._has_error = True
continue
if rel_page['id'] in existing: if rel_page['id'] in existing:
continue continue
try: try:
post_data = {'id': rel_page['id']} post_data = {'id': rel_page['id']}
endpoint.post(post_data) endpoint.post(post_data)
log.error("endpoint: %s, id: %s", endpoint.endpoint, rel_page['id']) log.error("endpoint: %s, id: %s", endpoint.endpoint, rel_page['id'])
self._has_error = True
except exc.NoContent: # desired exception on successful (dis)association except exc.NoContent: # desired exception on successful (dis)association
pass pass
except exc.Common as e: except exc.Common as e:
log.error("Object association failed: %s.", e) log.error("Object association failed: %s.", e)
self._has_error = True
log.debug("post_data: %r", post_data) log.debug("post_data: %r", post_data)
else: # It is a create set else: # It is a create set
self._cache.get_page(endpoint) self._cache.get_page(endpoint)

Some files were not shown because too many files have changed in this diff Show More