mirror of
https://github.com/ansible/awx.git
synced 2026-04-04 09:45:06 -02:30
Compare commits
1 Commits
21.8.0
...
12824-Inst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea8b1a754 |
20
.github/workflows/pr_body_check.yml
vendored
20
.github/workflows/pr_body_check.yml
vendored
@@ -13,13 +13,21 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Check for each of the lines
|
- name: Write PR body to a file
|
||||||
env:
|
|
||||||
PR_BODY: ${{ github.event.pull_request.body }}
|
|
||||||
run: |
|
run: |
|
||||||
echo $PR_BODY | grep "Bug, Docs Fix or other nominal change" > Z
|
cat >> pr.body << __SOME_RANDOM_PR_EOF__
|
||||||
echo $PR_BODY | grep "New or Enhanced Feature" > Y
|
${{ github.event.pull_request.body }}
|
||||||
echo $PR_BODY | grep "Breaking Change" > X
|
__SOME_RANDOM_PR_EOF__
|
||||||
|
|
||||||
|
- name: Display the received body for troubleshooting
|
||||||
|
run: cat pr.body
|
||||||
|
|
||||||
|
# We want to write these out individually just incase the options were joined on a single line
|
||||||
|
- name: Check for each of the lines
|
||||||
|
run: |
|
||||||
|
grep "Bug, Docs Fix or other nominal change" pr.body > Z
|
||||||
|
grep "New or Enhanced Feature" pr.body > Y
|
||||||
|
grep "Breaking Change" pr.body > X
|
||||||
exit 0
|
exit 0
|
||||||
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
|
# We exit 0 and set the shell to prevent the returns from the greps from failing this step
|
||||||
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||||
|
|||||||
24
Makefile
24
Makefile
@@ -85,7 +85,6 @@ clean: clean-ui clean-api clean-awxkit clean-dist
|
|||||||
|
|
||||||
clean-api:
|
clean-api:
|
||||||
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
rm -rf build $(NAME)-$(VERSION) *.egg-info
|
||||||
rm -rf .tox
|
|
||||||
find . -type f -regex ".*\.py[co]$$" -delete
|
find . -type f -regex ".*\.py[co]$$" -delete
|
||||||
find . -type d -name "__pycache__" -delete
|
find . -type d -name "__pycache__" -delete
|
||||||
rm -f awx/awx_test.sqlite3*
|
rm -f awx/awx_test.sqlite3*
|
||||||
@@ -182,7 +181,7 @@ collectstatic:
|
|||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/awx/bin/activate; \
|
. $(VENV_BASE)/awx/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
$(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
|
mkdir -p awx/public/static && $(PYTHON) manage.py collectstatic --clear --noinput > /dev/null 2>&1
|
||||||
|
|
||||||
DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:*
|
DEV_RELOAD_COMMAND ?= supervisorctl restart tower-processes:*
|
||||||
|
|
||||||
@@ -378,8 +377,6 @@ clean-ui:
|
|||||||
rm -rf awx/ui/build
|
rm -rf awx/ui/build
|
||||||
rm -rf awx/ui/src/locales/_build
|
rm -rf awx/ui/src/locales/_build
|
||||||
rm -rf $(UI_BUILD_FLAG_FILE)
|
rm -rf $(UI_BUILD_FLAG_FILE)
|
||||||
# the collectstatic command doesn't like it if this dir doesn't exist.
|
|
||||||
mkdir -p awx/ui/build/static
|
|
||||||
|
|
||||||
awx/ui/node_modules:
|
awx/ui/node_modules:
|
||||||
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
|
NODE_OPTIONS=--max-old-space-size=6144 $(NPM_BIN) --prefix awx/ui --loglevel warn --force ci
|
||||||
@@ -389,14 +386,16 @@ $(UI_BUILD_FLAG_FILE):
|
|||||||
$(PYTHON) tools/scripts/compilemessages.py
|
$(PYTHON) tools/scripts/compilemessages.py
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run compile-strings
|
||||||
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
$(NPM_BIN) --prefix awx/ui --loglevel warn run build
|
||||||
mkdir -p /var/lib/awx/public/static/css
|
mkdir -p awx/public/static/css
|
||||||
mkdir -p /var/lib/awx/public/static/js
|
mkdir -p awx/public/static/js
|
||||||
mkdir -p /var/lib/awx/public/static/media
|
mkdir -p awx/public/static/media
|
||||||
cp -r awx/ui/build/static/css/* /var/lib/awx/public/static/css
|
cp -r awx/ui/build/static/css/* awx/public/static/css
|
||||||
cp -r awx/ui/build/static/js/* /var/lib/awx/public/static/js
|
cp -r awx/ui/build/static/js/* awx/public/static/js
|
||||||
cp -r awx/ui/build/static/media/* /var/lib/awx/public/static/media
|
cp -r awx/ui/build/static/media/* awx/public/static/media
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ui-release: $(UI_BUILD_FLAG_FILE)
|
ui-release: $(UI_BUILD_FLAG_FILE)
|
||||||
|
|
||||||
ui-devel: awx/ui/node_modules
|
ui-devel: awx/ui/node_modules
|
||||||
@@ -454,7 +453,6 @@ COMPOSE_OPTS ?=
|
|||||||
CONTROL_PLANE_NODE_COUNT ?= 1
|
CONTROL_PLANE_NODE_COUNT ?= 1
|
||||||
EXECUTION_NODE_COUNT ?= 2
|
EXECUTION_NODE_COUNT ?= 2
|
||||||
MINIKUBE_CONTAINER_GROUP ?= false
|
MINIKUBE_CONTAINER_GROUP ?= false
|
||||||
MINIKUBE_SETUP ?= false # if false, run minikube separately
|
|
||||||
EXTRA_SOURCES_ANSIBLE_OPTS ?=
|
EXTRA_SOURCES_ANSIBLE_OPTS ?=
|
||||||
|
|
||||||
ifneq ($(ADMIN_PASSWORD),)
|
ifneq ($(ADMIN_PASSWORD),)
|
||||||
@@ -463,7 +461,7 @@ endif
|
|||||||
|
|
||||||
docker-compose-sources: .git/hooks/pre-commit
|
docker-compose-sources: .git/hooks/pre-commit
|
||||||
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
||||||
ansible-playbook -i tools/docker-compose/inventory -e minikube_setup=$(MINIKUBE_SETUP) tools/docker-compose-minikube/deploy.yml; \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose-minikube/deploy.yml; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
||||||
@@ -637,4 +635,4 @@ help/generate:
|
|||||||
} \
|
} \
|
||||||
} \
|
} \
|
||||||
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
|
{ lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u
|
||||||
@printf "\n"
|
@printf "\n"
|
||||||
@@ -4952,7 +4952,7 @@ class InstanceSerializer(BaseSerializer):
|
|||||||
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
|
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
|
||||||
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
|
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
|
||||||
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
|
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
|
||||||
if obj.node_type == 'execution':
|
if obj.node_type != 'hop':
|
||||||
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
|
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|||||||
@@ -392,8 +392,8 @@ class InstanceHealthCheck(GenericAPIView):
|
|||||||
permission_classes = (IsSystemAdminOrAuditor,)
|
permission_classes = (IsSystemAdminOrAuditor,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(node_type='execution')
|
|
||||||
# FIXME: For now, we don't have a good way of checking the health of a hop node.
|
# FIXME: For now, we don't have a good way of checking the health of a hop node.
|
||||||
|
return super().get_queryset().exclude(node_type='hop')
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@@ -413,10 +413,9 @@ class InstanceHealthCheck(GenericAPIView):
|
|||||||
|
|
||||||
execution_node_health_check.apply_async([obj.hostname])
|
execution_node_health_check.apply_async([obj.hostname])
|
||||||
else:
|
else:
|
||||||
return Response(
|
from awx.main.tasks.system import cluster_node_health_check
|
||||||
{"error": f"Cannot run a health check on instances of type {obj.node_type}. Health checks can only be run on execution nodes."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
cluster_node_health_check.apply_async([obj.hostname], queue=obj.hostname)
|
||||||
)
|
|
||||||
return Response({'msg': f"Health check is running for {obj.hostname}."}, status=status.HTTP_200_OK)
|
return Response({'msg': f"Health check is running for {obj.hostname}."}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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
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
@@ -247,19 +247,6 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
|
|||||||
return (number, step)
|
return (number, step)
|
||||||
|
|
||||||
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
|
def get_sliced_hosts(self, host_queryset, slice_number, slice_count):
|
||||||
"""
|
|
||||||
Returns a slice of Hosts given a slice number and total slice count, or
|
|
||||||
the original queryset if slicing is not requested.
|
|
||||||
|
|
||||||
NOTE: If slicing is performed, this will return a List[Host] with the
|
|
||||||
resulting slice. If slicing is not performed it will return the
|
|
||||||
original queryset (not evaluating it or forcing it to a list). This
|
|
||||||
puts the burden on the caller to check the resulting type. This is
|
|
||||||
non-ideal because it's easy to get wrong, but I think the only way
|
|
||||||
around it is to force the queryset which has memory implications for
|
|
||||||
large inventories.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if slice_count > 1 and slice_number > 0:
|
if slice_count > 1 and slice_number > 0:
|
||||||
offset = slice_number - 1
|
offset = slice_number - 1
|
||||||
host_queryset = host_queryset[offset::slice_count]
|
host_queryset = host_queryset[offset::slice_count]
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from urllib.parse import urljoin
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
# from django.core.cache import cache
|
# from django.core.cache import cache
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
@@ -845,30 +844,22 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
|||||||
def get_notification_friendly_name(self):
|
def get_notification_friendly_name(self):
|
||||||
return "Job"
|
return "Job"
|
||||||
|
|
||||||
def _get_inventory_hosts(self, only=('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id'), **filters):
|
def _get_inventory_hosts(self, only=['name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id']):
|
||||||
"""Return value is an iterable for the relevant hosts for this job"""
|
|
||||||
if not self.inventory:
|
if not self.inventory:
|
||||||
return []
|
return []
|
||||||
host_queryset = self.inventory.hosts.only(*only)
|
host_queryset = self.inventory.hosts.only(*only)
|
||||||
if filters:
|
return self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
|
||||||
host_queryset = host_queryset.filter(**filters)
|
|
||||||
host_queryset = self.inventory.get_sliced_hosts(host_queryset, self.job_slice_number, self.job_slice_count)
|
|
||||||
if isinstance(host_queryset, QuerySet):
|
|
||||||
return host_queryset.iterator()
|
|
||||||
return host_queryset
|
|
||||||
|
|
||||||
def start_job_fact_cache(self, destination, modification_times, timeout=None):
|
def start_job_fact_cache(self, destination, modification_times, timeout=None):
|
||||||
self.log_lifecycle("start_job_fact_cache")
|
self.log_lifecycle("start_job_fact_cache")
|
||||||
os.makedirs(destination, mode=0o700)
|
os.makedirs(destination, mode=0o700)
|
||||||
|
hosts = self._get_inventory_hosts()
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
|
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
|
||||||
if timeout > 0:
|
if timeout > 0:
|
||||||
# exclude hosts with fact data older than `settings.ANSIBLE_FACT_CACHE_TIMEOUT seconds`
|
# exclude hosts with fact data older than `settings.ANSIBLE_FACT_CACHE_TIMEOUT seconds`
|
||||||
timeout = now() - datetime.timedelta(seconds=timeout)
|
timeout = now() - datetime.timedelta(seconds=timeout)
|
||||||
hosts = self._get_inventory_hosts(ansible_facts_modified__gte=timeout)
|
hosts = hosts.filter(ansible_facts_modified__gte=timeout)
|
||||||
else:
|
|
||||||
hosts = self._get_inventory_hosts()
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
filepath = os.sep.join(map(str, [destination, host.name]))
|
filepath = os.sep.join(map(str, [destination, host.name]))
|
||||||
if not os.path.realpath(filepath).startswith(destination):
|
if not os.path.realpath(filepath).startswith(destination):
|
||||||
|
|||||||
@@ -208,10 +208,7 @@ def run_until_complete(node, timing_data=None, **kwargs):
|
|||||||
if state_name.lower() == 'failed':
|
if state_name.lower() == 'failed':
|
||||||
work_detail = status.get('Detail', '')
|
work_detail = status.get('Detail', '')
|
||||||
if work_detail:
|
if work_detail:
|
||||||
if stdout:
|
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
|
||||||
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}\nstdout:\n{stdout}')
|
|
||||||
else:
|
|
||||||
raise RemoteJobError(f'Receptor error from {node}, detail:\n{work_detail}')
|
|
||||||
else:
|
else:
|
||||||
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')
|
raise RemoteJobError(f'Unknown ansible-runner error on node {node}, stdout:\n{stdout}')
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from awx.main.models.ha import Instance
|
|||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, node_type='execution', memory=36000000000, cpu_capacity=6, mem_capacity=42)
|
INSTANCE_KWARGS = dict(hostname='example-host', cpu=6, memory=36000000000, cpu_capacity=6, mem_capacity=42)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ if settings.COLOR_LOGS is True:
|
|||||||
# logs rendered with cyan text
|
# logs rendered with cyan text
|
||||||
previous_level_map = self.level_map.copy()
|
previous_level_map = self.level_map.copy()
|
||||||
if record.name == "awx.analytics.job_lifecycle":
|
if record.name == "awx.analytics.job_lifecycle":
|
||||||
self.level_map[logging.INFO] = (None, 'cyan', True)
|
self.level_map[logging.DEBUG] = (None, 'cyan', True)
|
||||||
msg = super(ColorHandler, self).colorize(line, record)
|
msg = super(ColorHandler, self).colorize(line, record)
|
||||||
self.level_map = previous_level_map
|
self.level_map = previous_level_map
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'ui', 'build', 'static'), os.path.join(BASE_DIR, 'static')]
|
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'ui', 'build', 'static'), os.path.join(BASE_DIR, 'static'))
|
||||||
|
|
||||||
# Absolute filesystem path to the directory where static file are collected via
|
# Absolute filesystem path to the directory where static file are collected via
|
||||||
# the collectstatic command.
|
# the collectstatic command.
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { arrayOf, bool, number, shape, string } from 'prop-types';
|
||||||
|
|
||||||
|
import { Label, LabelGroup } from '@patternfly/react-core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function InstanceGroupLabels({ labels, isLinkable }) {
|
||||||
|
const buildLinkURL = (isContainerGroup) =>
|
||||||
|
isContainerGroup
|
||||||
|
? '/instance_groups/container_group/'
|
||||||
|
: '/instance_groups/';
|
||||||
|
return (
|
||||||
|
<LabelGroup numLabels={5}>
|
||||||
|
{labels.map(({ id, name, is_container_group }) =>
|
||||||
|
isLinkable ? (
|
||||||
|
<Label
|
||||||
|
color="blue"
|
||||||
|
key={id}
|
||||||
|
render={({ className, content, componentRef }) => (
|
||||||
|
<Link
|
||||||
|
className={className}
|
||||||
|
innerRef={componentRef}
|
||||||
|
to={`${buildLinkURL(is_container_group)}${id}/details`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Label>
|
||||||
|
) : (
|
||||||
|
<Label color="blue" key={id}>
|
||||||
|
{name}
|
||||||
|
</Label>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</LabelGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceGroupLabels.propTypes = {
|
||||||
|
labels: arrayOf(shape({ id: number.isRequired, name: string.isRequired }))
|
||||||
|
.isRequired,
|
||||||
|
isLinkable: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
InstanceGroupLabels.defaultProps = { isLinkable: false };
|
||||||
|
|
||||||
|
export default InstanceGroupLabels;
|
||||||
1
awx/ui/src/components/InstanceGroupLabels/index.js
Normal file
1
awx/ui/src/components/InstanceGroupLabels/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './InstanceGroupLabels';
|
||||||
@@ -6,6 +6,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Chip, Divider, Title } from '@patternfly/react-core';
|
import { Chip, Divider, Title } from '@patternfly/react-core';
|
||||||
import { toTitleCase } from 'util/strings';
|
import { toTitleCase } from 'util/strings';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import CredentialChip from '../CredentialChip';
|
import CredentialChip from '../CredentialChip';
|
||||||
import ChipGroup from '../ChipGroup';
|
import ChipGroup from '../ChipGroup';
|
||||||
import { DetailList, Detail, UserDateDetail } from '../DetailList';
|
import { DetailList, Detail, UserDateDetail } from '../DetailList';
|
||||||
@@ -227,21 +228,7 @@ function PromptDetail({
|
|||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
rows={4}
|
rows={4}
|
||||||
value={
|
value={
|
||||||
<ChipGroup
|
<InstanceGroupLabels labels={overrides.instance_groups} />
|
||||||
numChips={5}
|
|
||||||
totalChips={overrides.instance_groups.length}
|
|
||||||
ouiaId="prompt-instance-groups-chips"
|
|
||||||
>
|
|
||||||
{overrides.instance_groups.map((instance_group) => (
|
|
||||||
<Chip
|
|
||||||
key={instance_group.id}
|
|
||||||
ouiaId={`instance-group-${instance_group.id}-chip`}
|
|
||||||
isReadOnly
|
|
||||||
>
|
|
||||||
{instance_group.name}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { useConfig } from 'contexts/Config';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import parseRuleObj from '../shared/parseRuleObj';
|
import parseRuleObj from '../shared/parseRuleObj';
|
||||||
import FrequencyDetails from './FrequencyDetails';
|
import FrequencyDetails from './FrequencyDetails';
|
||||||
import AlertModal from '../../AlertModal';
|
import AlertModal from '../../AlertModal';
|
||||||
@@ -27,11 +28,6 @@ import { VariablesDetail } from '../../CodeEditor';
|
|||||||
import { VERBOSITY } from '../../VerbositySelectField';
|
import { VERBOSITY } from '../../VerbositySelectField';
|
||||||
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
|
||||||
|
|
||||||
const buildLinkURL = (instance) =>
|
|
||||||
instance.is_container_group
|
|
||||||
? '/instance_groups/container_group/'
|
|
||||||
: '/instance_groups/';
|
|
||||||
|
|
||||||
const PromptDivider = styled(Divider)`
|
const PromptDivider = styled(Divider)`
|
||||||
margin-top: var(--pf-global--spacer--lg);
|
margin-top: var(--pf-global--spacer--lg);
|
||||||
margin-bottom: var(--pf-global--spacer--lg);
|
margin-bottom: var(--pf-global--spacer--lg);
|
||||||
@@ -498,26 +494,7 @@ function ScheduleDetail({ hasDaysToKeepField, schedule, surveyConfig }) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
value={
|
value={
|
||||||
<ChipGroup
|
<InstanceGroupLabels labels={instanceGroups} isLinkable />
|
||||||
numChips={5}
|
|
||||||
totalChips={instanceGroups.length}
|
|
||||||
ouiaId="instance-group-chips"
|
|
||||||
>
|
|
||||||
{instanceGroups.map((ig) => (
|
|
||||||
<Link
|
|
||||||
to={`${buildLinkURL(ig)}${ig.id}/details`}
|
|
||||||
key={ig.id}
|
|
||||||
>
|
|
||||||
<Chip
|
|
||||||
key={ig.id}
|
|
||||||
ouiaId={`instance-group-${ig.id}-chip`}
|
|
||||||
isReadOnly
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Chip>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
}
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -416,14 +416,8 @@ function ScheduleForm({
|
|||||||
|
|
||||||
if (options.end === 'onDate') {
|
if (options.end === 'onDate') {
|
||||||
if (
|
if (
|
||||||
DateTime.fromFormat(
|
DateTime.fromISO(values.startDate) >=
|
||||||
`${values.startDate} ${values.startTime}`,
|
DateTime.fromISO(options.endDate)
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis() >=
|
|
||||||
DateTime.fromFormat(
|
|
||||||
`${options.endDate} ${options.endTime}`,
|
|
||||||
'yyyy-LL-dd h:mm a'
|
|
||||||
).toMillis()
|
|
||||||
) {
|
) {
|
||||||
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -900,36 +900,6 @@ describe('<ScheduleForm />', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create schedule with the same start and end date provided that the end date is at a later time', async () => {
|
|
||||||
const today = DateTime.now().toFormat('yyyy-LL-dd');
|
|
||||||
const laterTime = DateTime.now().plus({ hours: 1 }).toFormat('h:mm a');
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('DatePicker[aria-label="End date"]').prop('onChange')(
|
|
||||||
today,
|
|
||||||
new Date(today)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
wrapper.update();
|
|
||||||
expect(
|
|
||||||
wrapper
|
|
||||||
.find('FormGroup[data-cy="schedule-End date/time"]')
|
|
||||||
.prop('helperTextInvalid')
|
|
||||||
).toBe(
|
|
||||||
'Please select an end date/time that comes after the start date/time.'
|
|
||||||
);
|
|
||||||
await act(async () => {
|
|
||||||
wrapper.find('TimePicker[aria-label="End time"]').prop('onChange')(
|
|
||||||
laterTime
|
|
||||||
);
|
|
||||||
});
|
|
||||||
wrapper.update();
|
|
||||||
expect(
|
|
||||||
wrapper
|
|
||||||
.find('FormGroup[data-cy="schedule-End date/time"]')
|
|
||||||
.prop('helperTextInvalid')
|
|
||||||
).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('error shown when on day number is not between 1 and 31', async () => {
|
test('error shown when on day number is not between 1 and 31', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
wrapper.find('FrequencySelect#schedule-frequency').invoke('onChange')([
|
||||||
|
|||||||
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
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
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { t, Plural } from '@lingui/macro';
|
import { t, Plural } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
CodeBlockCode,
|
CodeBlockCode,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Slider,
|
Slider,
|
||||||
Label,
|
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -34,6 +33,7 @@ import useRequest, {
|
|||||||
useDismissableError,
|
useDismissableError,
|
||||||
} from 'hooks/useRequest';
|
} from 'hooks/useRequest';
|
||||||
import HealthCheckAlert from 'components/HealthCheckAlert';
|
import HealthCheckAlert from 'components/HealthCheckAlert';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import RemoveInstanceButton from '../Shared/RemoveInstanceButton';
|
import RemoveInstanceButton from '../Shared/RemoveInstanceButton';
|
||||||
|
|
||||||
const Unavailable = styled.span`
|
const Unavailable = styled.span`
|
||||||
@@ -156,11 +156,6 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildLinkURL = (inst) =>
|
|
||||||
inst.is_container_group
|
|
||||||
? '/instance_groups/container_group/'
|
|
||||||
: '/instance_groups/';
|
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(
|
const { error, dismissError } = useDismissableError(
|
||||||
updateInstanceError || healthCheckError
|
updateInstanceError || healthCheckError
|
||||||
);
|
);
|
||||||
@@ -225,25 +220,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
dataCy="instance-groups"
|
dataCy="instance-groups"
|
||||||
helpText={t`The Instance Groups to which this instance belongs.`}
|
helpText={t`The Instance Groups to which this instance belongs.`}
|
||||||
value={instanceGroups.map((ig) => (
|
value={
|
||||||
<React.Fragment key={ig.id}>
|
<InstanceGroupLabels labels={instanceGroups} isLinkable />
|
||||||
<Label
|
}
|
||||||
color="blue"
|
|
||||||
isTruncated
|
|
||||||
render={({ className, content, componentRef }) => (
|
|
||||||
<Link
|
|
||||||
to={`${buildLinkURL(ig)}${ig.id}/details`}
|
|
||||||
className={className}
|
|
||||||
innerRef={componentRef}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Label>{' '}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { InventoriesAPI } from 'api';
|
|||||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||||
import { Inventory } from 'types';
|
import { Inventory } from 'types';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import getHelpText from '../shared/Inventory.helptext';
|
import getHelpText from '../shared/Inventory.helptext';
|
||||||
|
|
||||||
function InventoryDetail({ inventory }) {
|
function InventoryDetail({ inventory }) {
|
||||||
@@ -105,23 +106,7 @@ function InventoryDetail({ inventory }) {
|
|||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
value={
|
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
|
||||||
<ChipGroup
|
|
||||||
numChips={5}
|
|
||||||
totalChips={instanceGroups?.length}
|
|
||||||
ouiaId="instance-group-chips"
|
|
||||||
>
|
|
||||||
{instanceGroups?.map((ig) => (
|
|
||||||
<Chip
|
|
||||||
key={ig.id}
|
|
||||||
isReadOnly
|
|
||||||
ouiaId={`instance-group-${ig.id}-chip`}
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -131,9 +131,8 @@ describe('<InventoryDetail />', () => {
|
|||||||
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
|
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
|
||||||
mockInventory.id
|
mockInventory.id
|
||||||
);
|
);
|
||||||
const chip = wrapper.find('Chip').at(0);
|
const label = wrapper.find('Label').at(0);
|
||||||
expect(chip.prop('isReadOnly')).toEqual(true);
|
expect(label.prop('children')).toEqual('Foo');
|
||||||
expect(chip.prop('children')).toEqual('Foo');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not load instance groups', async () => {
|
test('should not load instance groups', async () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
|
|||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button, Chip, Label } from '@patternfly/react-core';
|
import { Button, Label } from '@patternfly/react-core';
|
||||||
|
|
||||||
import { Inventory } from 'types';
|
import { Inventory } from 'types';
|
||||||
import { InventoriesAPI, UnifiedJobsAPI } from 'api';
|
import { InventoriesAPI, UnifiedJobsAPI } from 'api';
|
||||||
@@ -10,7 +10,6 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
|
|||||||
|
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
import ChipGroup from 'components/ChipGroup';
|
|
||||||
import { VariablesDetail } from 'components/CodeEditor';
|
import { VariablesDetail } from 'components/CodeEditor';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
@@ -18,6 +17,7 @@ import DeleteButton from 'components/DeleteButton';
|
|||||||
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import Sparkline from 'components/Sparkline';
|
import Sparkline from 'components/Sparkline';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
|
|
||||||
function SmartInventoryDetail({ inventory }) {
|
function SmartInventoryDetail({ inventory }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -120,23 +120,7 @@ function SmartInventoryDetail({ inventory }) {
|
|||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
fullWidth
|
||||||
label={t`Instance groups`}
|
label={t`Instance groups`}
|
||||||
value={
|
value={<InstanceGroupLabels labels={instanceGroups} />}
|
||||||
<ChipGroup
|
|
||||||
numChips={5}
|
|
||||||
totalChips={instanceGroups.length}
|
|
||||||
ouiaId="instance-group-chips"
|
|
||||||
>
|
|
||||||
{instanceGroups.map((ig) => (
|
|
||||||
<Chip
|
|
||||||
key={ig.id}
|
|
||||||
isReadOnly
|
|
||||||
ouiaId={`instance-group-${ig.id}-chip`}
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
<VariablesDetail
|
<VariablesDetail
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
|
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button, Chip } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { OrganizationsAPI } from 'api';
|
import { OrganizationsAPI } from 'api';
|
||||||
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
@@ -16,6 +16,7 @@ import ErrorDetail from 'components/ErrorDetail';
|
|||||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
|
|
||||||
function OrganizationDetail({ organization }) {
|
function OrganizationDetail({ organization }) {
|
||||||
@@ -79,11 +80,6 @@ function OrganizationDetail({ organization }) {
|
|||||||
return <ContentError error={contentError} />;
|
return <ContentError error={contentError} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildLinkURL = (instance) =>
|
|
||||||
instance.is_container_group
|
|
||||||
? '/instance_groups/container_group/'
|
|
||||||
: '/instance_groups/';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList>
|
||||||
@@ -126,25 +122,7 @@ function OrganizationDetail({ organization }) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
helpText={t`The Instance Groups for this Organization to run on.`}
|
helpText={t`The Instance Groups for this Organization to run on.`}
|
||||||
value={
|
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
|
||||||
<ChipGroup
|
|
||||||
numChips={5}
|
|
||||||
totalChips={instanceGroups.length}
|
|
||||||
ouiaId="instance-group-chips"
|
|
||||||
>
|
|
||||||
{instanceGroups.map((ig) => (
|
|
||||||
<Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}>
|
|
||||||
<Chip
|
|
||||||
key={ig.id}
|
|
||||||
isReadOnly
|
|
||||||
ouiaId={`instance-group-${ig.id}-chip`}
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Chip>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe('<OrganizationDetail />', () => {
|
|||||||
await waitForElement(component, 'ContentLoading', (el) => el.length === 0);
|
await waitForElement(component, 'ContentLoading', (el) => el.length === 0);
|
||||||
expect(
|
expect(
|
||||||
component
|
component
|
||||||
.find('Chip')
|
.find('Label')
|
||||||
.findWhere((el) => el.text() === 'One')
|
.findWhere((el) => el.text() === 'One')
|
||||||
.exists()
|
.exists()
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import useRequest, { useDismissableError } from 'hooks/useRequest';
|
|||||||
import useBrandName from 'hooks/useBrandName';
|
import useBrandName from 'hooks/useBrandName';
|
||||||
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
import getHelpText from '../shared/JobTemplate.helptext';
|
import getHelpText from '../shared/JobTemplate.helptext';
|
||||||
|
|
||||||
function JobTemplateDetail({ template }) {
|
function JobTemplateDetail({ template }) {
|
||||||
@@ -167,11 +168,6 @@ function JobTemplateDetail({ template }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLinkURL = (instance) =>
|
|
||||||
instance.is_container_group
|
|
||||||
? '/instance_groups/container_group/'
|
|
||||||
: '/instance_groups/';
|
|
||||||
|
|
||||||
if (instanceGroupsError) {
|
if (instanceGroupsError) {
|
||||||
return <ContentError error={instanceGroupsError} />;
|
return <ContentError error={instanceGroupsError} />;
|
||||||
}
|
}
|
||||||
@@ -422,25 +418,7 @@ function JobTemplateDetail({ template }) {
|
|||||||
label={t`Instance Groups`}
|
label={t`Instance Groups`}
|
||||||
dataCy="jt-detail-instance-groups"
|
dataCy="jt-detail-instance-groups"
|
||||||
helpText={helpText.instanceGroups}
|
helpText={helpText.instanceGroups}
|
||||||
value={
|
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
|
||||||
<ChipGroup
|
|
||||||
numChips={5}
|
|
||||||
totalChips={instanceGroups.length}
|
|
||||||
ouiaId="instance-group-chips"
|
|
||||||
>
|
|
||||||
{instanceGroups.map((ig) => (
|
|
||||||
<Link to={`${buildLinkURL(ig)}${ig.id}/details`} key={ig.id}>
|
|
||||||
<Chip
|
|
||||||
key={ig.id}
|
|
||||||
ouiaId={`instance-group-${ig.id}-chip`}
|
|
||||||
isReadOnly
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Chip>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
isEmpty={instanceGroups.length === 0}
|
||||||
/>
|
/>
|
||||||
{job_tags && (
|
{job_tags && (
|
||||||
|
|||||||
@@ -114,12 +114,7 @@ def main():
|
|||||||
# Update the project
|
# Update the project
|
||||||
result = module.post_endpoint(project['related']['update'])
|
result = module.post_endpoint(project['related']['update'])
|
||||||
|
|
||||||
if result['status_code'] == 405:
|
if result['status_code'] != 202:
|
||||||
module.fail_json(
|
|
||||||
msg="Unable to trigger a project update because the project scm_type ({0}) does not support it.".format(project['scm_type']),
|
|
||||||
response=result
|
|
||||||
)
|
|
||||||
elif result['status_code'] != 202:
|
|
||||||
module.fail_json(msg="Failed to update project, see response for details", response=result)
|
module.fail_json(msg="Failed to update project, see response for details", response=result)
|
||||||
|
|
||||||
module.json_output['changed'] = True
|
module.json_output['changed'] = True
|
||||||
|
|||||||
@@ -275,13 +275,7 @@ class ApiV2(base.Base):
|
|||||||
# When creating a project, we need to wait for its
|
# When creating a project, we need to wait for its
|
||||||
# first project update to finish so that associated
|
# first project update to finish so that associated
|
||||||
# JTs have valid options for playbook names
|
# JTs have valid options for playbook names
|
||||||
try:
|
_page.wait_until_completed()
|
||||||
_page.wait_until_completed(timeout=300)
|
|
||||||
except AssertionError:
|
|
||||||
# If the project update times out, try to
|
|
||||||
# carry on in the hopes that it will
|
|
||||||
# finish before it is needed.
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
# If we are an existing project and our scm_tpye is not changing don't try and import the local_path setting
|
# If we are an existing project and our scm_tpye is not changing don't try and import the local_path setting
|
||||||
if asset['natural_key']['type'] == 'project' and 'local_path' in post_data and _page['scm_type'] == post_data['scm_type']:
|
if asset['natural_key']['type'] == 'project' and 'local_path' in post_data and _page['scm_type'] == post_data['scm_type']:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
from awxkit.api.pages import JobTemplate, SystemJobTemplate, Project, InventorySource
|
from awxkit.api.pages import SystemJobTemplate
|
||||||
from awxkit.api.pages.workflow_job_templates import WorkflowJobTemplate
|
|
||||||
from awxkit.api.mixins import HasCreate
|
from awxkit.api.mixins import HasCreate
|
||||||
from awxkit.api.resources import resources
|
from awxkit.api.resources import resources
|
||||||
from awxkit.config import config
|
from awxkit.config import config
|
||||||
@@ -12,7 +11,7 @@ from . import base
|
|||||||
|
|
||||||
|
|
||||||
class Schedule(HasCreate, base.Base):
|
class Schedule(HasCreate, base.Base):
|
||||||
dependencies = [JobTemplate, SystemJobTemplate, Project, InventorySource, WorkflowJobTemplate]
|
dependencies = [SystemJobTemplate]
|
||||||
NATURAL_KEY = ('unified_job_template', 'name')
|
NATURAL_KEY = ('unified_job_template', 'name')
|
||||||
|
|
||||||
def silent_delete(self):
|
def silent_delete(self):
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ template_dest: '_build'
|
|||||||
receptor_image: quay.io/ansible/receptor:devel
|
receptor_image: quay.io/ansible/receptor:devel
|
||||||
|
|
||||||
# Helper vars to construct the proper download URL for the current architecture
|
# Helper vars to construct the proper download URL for the current architecture
|
||||||
image_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm", "arm64": "arm64", "ppc64le": "ppc64le" }[ansible_facts.architecture] }}'
|
image_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm", "ppc64le": "ppc64le" }[ansible_facts.architecture] }}'
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ addons:
|
|||||||
minikube_url_linux: 'https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64'
|
minikube_url_linux: 'https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64'
|
||||||
minikube_url_macos: 'https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64'
|
minikube_url_macos: 'https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64'
|
||||||
|
|
||||||
kubectl_url_linux: 'https://dl.k8s.io/release/v1.25.0/bin/linux/amd64/kubectl'
|
kubectl_url_linux: 'https://dl.k8s.io/release/v1.21.0/bin/linux/amd64/kubectl'
|
||||||
kubectl_url_macos: 'https://dl.k8s.io/release/v1.25.0/bin/darwin/amd64/kubectl'
|
kubectl_url_macos: 'https://dl.k8s.io/release/v1.21.0/bin/darwin/amd64/kubectl'
|
||||||
|
|
||||||
# Service Account Name
|
# Service Account Name
|
||||||
minikube_service_account_name: 'awx-devel'
|
minikube_service_account_name: 'awx-devel'
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
state: 'directory'
|
state: 'directory'
|
||||||
mode: '0700'
|
mode: '0700'
|
||||||
|
|
||||||
- name: debug minikube_setup
|
|
||||||
debug:
|
|
||||||
var: minikube_setup
|
|
||||||
|
|
||||||
# Linux block
|
# Linux block
|
||||||
- block:
|
- block:
|
||||||
- name: Download Minikube
|
- name: Download Minikube
|
||||||
@@ -28,7 +24,6 @@
|
|||||||
when:
|
when:
|
||||||
- ansible_architecture == "x86_64"
|
- ansible_architecture == "x86_64"
|
||||||
- ansible_system == "Linux"
|
- ansible_system == "Linux"
|
||||||
- minikube_setup | default(False) | bool
|
|
||||||
|
|
||||||
# MacOS block
|
# MacOS block
|
||||||
- block:
|
- block:
|
||||||
@@ -46,29 +41,25 @@
|
|||||||
when:
|
when:
|
||||||
- ansible_architecture == "x86_64"
|
- ansible_architecture == "x86_64"
|
||||||
- ansible_system == "Darwin"
|
- ansible_system == "Darwin"
|
||||||
- minikube_setup | default(False) | bool
|
|
||||||
|
|
||||||
- block:
|
- name: Starting Minikube
|
||||||
- name: Starting Minikube
|
shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
|
||||||
shell: "{{ sources_dest }}/minikube start --driver={{ driver }} --install-addons=true --addons={{ addons | join(',') }}"
|
register: minikube_stdout
|
||||||
register: minikube_stdout
|
|
||||||
|
|
||||||
- name: Enable Ingress Controller on Minikube
|
- name: Enable Ingress Controller on Minikube
|
||||||
shell: "{{ sources_dest }}/minikube addons enable ingress"
|
shell: "{{ sources_dest }}/minikube addons enable ingress"
|
||||||
when:
|
|
||||||
- minikube_stdout.rc == 0
|
|
||||||
register: _minikube_ingress
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Show Minikube Ingress known-issue 7332 warning
|
|
||||||
pause:
|
|
||||||
seconds: 5
|
|
||||||
prompt: "The Minikube Ingress addon has been disabled since it looks like you are hitting https://github.com/kubernetes/minikube/issues/7332"
|
|
||||||
when:
|
|
||||||
- '"minikube/issues/7332" in _minikube_ingress.stderr'
|
|
||||||
- ansible_system == "Darwin"
|
|
||||||
when:
|
when:
|
||||||
- minikube_setup | default(False) | bool
|
- minikube_stdout.rc == 0
|
||||||
|
register: _minikube_ingress
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Show Minikube Ingress known-issue 7332 warning
|
||||||
|
pause:
|
||||||
|
seconds: 5
|
||||||
|
prompt: "The Minikube Ingress addon has been disabled since it looks like you are hitting https://github.com/kubernetes/minikube/issues/7332"
|
||||||
|
when:
|
||||||
|
- '"minikube/issues/7332" in _minikube_ingress.stderr'
|
||||||
|
- ansible_system == "Darwin"
|
||||||
|
|
||||||
- name: Create ServiceAccount and clusterRoleBinding
|
- name: Create ServiceAccount and clusterRoleBinding
|
||||||
k8s:
|
k8s:
|
||||||
|
|||||||
@@ -301,19 +301,11 @@ Note that you may see multiple messages of the form `2021-03-04 20:11:47,666 WAR
|
|||||||
|
|
||||||
To bring up a 1 node AWX + minikube that is accessible from AWX run the following.
|
To bring up a 1 node AWX + minikube that is accessible from AWX run the following.
|
||||||
|
|
||||||
Start minikube
|
|
||||||
|
|
||||||
```bash
|
|
||||||
(host)$minikube start --cpus=4 --memory=8g --addons=ingress`
|
|
||||||
```
|
|
||||||
|
|
||||||
Start AWX
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
(host)$ make docker-compose-container-group
|
(host)$ make docker-compose-container-group
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can set the env var `MINIKUBE_CONTAINER_GROUP=true` to use the default dev env bring up. his way you can use other env flags like the cluster node count. Set `MINIKUBE_SETUP=true` to make the roles download, install and run minikube for you, but if you run into issues with this just start minikube yourself.
|
Alternatively, you can set the env var `MINIKUBE_CONTAINER_GROUP=true` to use the default dev env bring up. his way you can use other env flags like the cluster node count.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
(host)$ MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
(host)$ MINIKUBE_CONTAINER_GROUP=true make docker-compose
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ else
|
|||||||
wait-for-migrations
|
wait-for-migrations
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make sure that the UI static file directory exists, Django complains otherwise.
|
|
||||||
mkdir -p /awx_devel/awx/ui/build/static
|
|
||||||
|
|
||||||
if output=$(awx-manage createsuperuser --noinput --username=admin --email=admin@localhost 2> /dev/null); then
|
if output=$(awx-manage createsuperuser --noinput --username=admin --email=admin@localhost 2> /dev/null); then
|
||||||
echo $output
|
echo $output
|
||||||
fi
|
fi
|
||||||
@@ -30,6 +27,10 @@ echo "Admin password: ${DJANGO_SUPERUSER_PASSWORD}"
|
|||||||
awx-manage create_preload_data
|
awx-manage create_preload_data
|
||||||
awx-manage register_default_execution_environments
|
awx-manage register_default_execution_environments
|
||||||
|
|
||||||
|
mkdir -p /awx_devel/awx/public/static
|
||||||
|
mkdir -p /awx_devel/awx/ui/static
|
||||||
|
mkdir -p /awx_devel/awx/ui/build/static
|
||||||
|
|
||||||
awx-manage provision_instance --hostname="$(hostname)" --node_type="$MAIN_NODE_TYPE"
|
awx-manage provision_instance --hostname="$(hostname)" --node_type="$MAIN_NODE_TYPE"
|
||||||
awx-manage register_queue --queuename=controlplane --instance_percent=100
|
awx-manage register_queue --queuename=controlplane --instance_percent=100
|
||||||
awx-manage register_queue --queuename=default --instance_percent=100
|
awx-manage register_queue --queuename=default --instance_percent=100
|
||||||
|
|||||||
Reference in New Issue
Block a user