Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Corey
bea8b1a754 Adds an Instance Group component that renders IGs as a PF Label 2022-10-26 13:36:14 -04:00
55 changed files with 140 additions and 204017 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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):

View File

@@ -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}')

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default } from './InstanceGroupLabels';

View File

@@ -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>
} }
/> />
)} )}

View File

@@ -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}
/> />

View File

@@ -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.`;
} }

View File

@@ -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

View File

@@ -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}
/> />
)} )}

View File

@@ -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}
/> />
)} )}

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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}
/> />
)} )}

View File

@@ -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);

View File

@@ -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 && (

View File

@@ -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

View File

@@ -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']:

View File

@@ -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):

View File

@@ -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] }}'

View File

@@ -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'

View File

@@ -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:

View File

@@ -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

View File

@@ -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