From adfd8ed26bcfd952b19c3d6fdc216cc9cb997e34 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 12 Nov 2020 13:35:05 -0500 Subject: [PATCH 01/55] forcibly close DB and cache socket connection post-fork we've seen evidence of a race condition on fork for awx.conf.Setting access; in the past, we've attempted to solve this by explicitly closing connections pre-fork, but we've seen evidence that this isn't always good enough this patch is an attempt to close connections post-fork so that sockets aren't inherited post fork, leading to bizarre race conditions in setting access --- awx/conf/settings.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/awx/conf/settings.py b/awx/conf/settings.py index d2733ce879..4b18e3d9f6 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -4,6 +4,7 @@ import logging import sys import threading import time +import os # Django from django.conf import LazySettings @@ -247,6 +248,7 @@ class SettingsWrapper(UserSettingsHolder): # These values have to be stored via self.__dict__ in this way to get # around the magic __setattr__ method on this class (which is used to # store API-assigned settings in the database). + self.__dict__['__forks__'] = {} self.__dict__['default_settings'] = default_settings self.__dict__['_awx_conf_settings'] = self self.__dict__['_awx_conf_preload_expires'] = None @@ -255,6 +257,26 @@ class SettingsWrapper(UserSettingsHolder): self.__dict__['cache'] = EncryptedCacheProxy(cache, registry) self.__dict__['registry'] = registry + # record the current pid so we compare it post-fork for + # processes like the dispatcher and callback receiver + self.__dict__['pid'] = os.getpid() + + def __clean_on_fork__(self): + pid = os.getpid() + # if the current pid does *not* match the value on self, it means + # that value was copied on fork, and we're now in a *forked* process; + # the *first* time we enter this code path (on setting access), + # forcibly close DB/cache sockets and set a marker so we don't run + # this code again _in this process_ + # + if pid != self.__dict__['pid'] and pid not in self.__dict__['__forks__']: + self.__dict__['__forks__'][pid] = True + # It's important to close these post-fork, because we + # don't want the forked processes to inherit the open sockets + # for the DB and cache connections (that way lies race conditions) + connection.close() + django_cache.close() + @cached_property def all_supported_settings(self): return self.registry.get_registered_settings() @@ -330,6 +352,7 @@ class SettingsWrapper(UserSettingsHolder): self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) def _get_local(self, name, validate=True): + self.__clean_on_fork__() self._preload_cache() cache_key = Setting.get_cache_key(name) try: From 8d6a6198dcb3c2187373dddfaf5578a8d9d8bf50 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Fri, 4 Dec 2020 16:10:48 -0500 Subject: [PATCH 02/55] Add Google OAuth 2.0 setting edit form --- .../GoogleOAuth2/GoogleOAuth2.test.jsx | 43 +++- .../GoogleOAuth2Edit/GoogleOAuth2Edit.jsx | 180 +++++++++++++++-- .../GoogleOAuth2Edit.test.jsx | 188 +++++++++++++++++- 3 files changed, 381 insertions(+), 30 deletions(-) diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx index 7b7b330aec..4455605098 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2.test.jsx @@ -2,13 +2,28 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; -import GoogleOAuth2 from './GoogleOAuth2'; - +import { SettingsProvider } from '../../../contexts/Settings'; import { SettingsAPI } from '../../../api'; +import mockAllOptions from '../shared/data.allSettingOptions.json'; +import GoogleOAuth2 from './GoogleOAuth2'; jest.mock('../../../api/models/Settings'); SettingsAPI.readCategory.mockResolvedValue({ - data: {}, + data: { + SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL: + 'https://towerhost/sso/complete/google-oauth2/', + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'mock key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '$encrypted$', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [ + 'example.com', + 'example_2.com', + ], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: {}, + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + }, }); describe('', () => { @@ -24,9 +39,14 @@ describe('', () => { initialEntries: ['/settings/google_oauth2/details'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('GoogleOAuth2Detail').length).toBe(1); }); @@ -36,9 +56,14 @@ describe('', () => { initialEntries: ['/settings/google_oauth2/edit'], }); await act(async () => { - wrapper = mountWithContexts(, { - context: { router: { history } }, - }); + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); }); expect(wrapper.find('GoogleOAuth2Edit').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.jsx index 50a546334d..ebc6f3d662 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.jsx @@ -1,25 +1,171 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; -import { CardBody, CardActionsRow } from '../../../../components/Card'; +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Formik } from 'formik'; +import { Form } from '@patternfly/react-core'; +import { CardBody } from '../../../../components/Card'; +import ContentError from '../../../../components/ContentError'; +import ContentLoading from '../../../../components/ContentLoading'; +import { FormSubmitError } from '../../../../components/FormField'; +import { FormColumnLayout } from '../../../../components/FormLayout'; +import { useSettings } from '../../../../contexts/Settings'; +import { RevertAllAlert, RevertFormActionGroup } from '../../shared'; +import { + EncryptedField, + InputField, + ObjectField, +} from '../../shared/SharedFields'; +import { formatJson } from '../../shared/settingUtils'; +import useModal from '../../../../util/useModal'; +import useRequest from '../../../../util/useRequest'; +import { SettingsAPI } from '../../../../api'; + +function GoogleOAuth2Edit() { + const history = useHistory(); + const { isModalOpen, toggleModal, closeModal } = useModal(); + const { PUT: options } = useSettings(); + + const { + isLoading, + error, + request: fetchGoogleOAuth2, + result: googleOAuth2, + } = useRequest( + useCallback(async () => { + const { data } = await SettingsAPI.readCategory('google-oauth2'); + const mergedData = {}; + Object.keys(data).forEach(key => { + if (!options[key]) { + return; + } + mergedData[key] = options[key]; + mergedData[key].value = data[key]; + }); + return mergedData; + }, [options]), + null + ); + + useEffect(() => { + fetchGoogleOAuth2(); + }, [fetchGoogleOAuth2]); + + const { error: submitError, request: submitForm } = useRequest( + useCallback( + async values => { + await SettingsAPI.updateAll(values); + history.push('/settings/google_oauth2/details'); + }, + [history] + ), + null + ); + + const handleSubmit = async form => { + await submitForm({ + ...form, + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: formatJson( + form.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS + ), + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: formatJson( + form.SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS + ), + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: formatJson( + form.SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP + ), + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: formatJson( + form.SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP + ), + }); + }; + + const handleRevertAll = async () => { + const defaultValues = Object.assign( + ...Object.entries(googleOAuth2).map(([key, value]) => ({ + [key]: value.default, + })) + ); + await submitForm(defaultValues); + closeModal(); + }; + + const handleCancel = () => { + history.push('/settings/google_oauth2/details'); + }; + + const initialValues = fields => + Object.keys(fields).reduce((acc, key) => { + if (fields[key].type === 'list' || fields[key].type === 'nested object') { + const emptyDefault = fields[key].type === 'list' ? '[]' : '{}'; + acc[key] = fields[key].value + ? JSON.stringify(fields[key].value, null, 2) + : emptyDefault; + } else { + acc[key] = fields[key].value ?? ''; + } + return acc; + }, {}); -function GoogleOAuth2Edit({ i18n }) { return ( - {i18n._(t`Edit form coming soon :)`)} - - - + {formik => ( +
+ + + + + + + + {submitError && } + + + {isModalOpen && ( + + )} + + )} + + )}
); } -export default withI18n()(GoogleOAuth2Edit); +export default GoogleOAuth2Edit; diff --git a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx index 034a0def4e..68a292232c 100644 --- a/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx +++ b/awx/ui_next/src/screens/Setting/GoogleOAuth2/GoogleOAuth2Edit/GoogleOAuth2Edit.test.jsx @@ -1,16 +1,196 @@ import React from 'react'; -import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; +import { + mountWithContexts, + waitForElement, +} from '../../../../../testUtils/enzymeHelpers'; +import { SettingsProvider } from '../../../../contexts/Settings'; +import { SettingsAPI } from '../../../../api'; +import mockAllOptions from '../../shared/data.allSettingOptions.json'; import GoogleOAuth2Edit from './GoogleOAuth2Edit'; +jest.mock('../../../../api/models/Settings'); +SettingsAPI.updateAll.mockResolvedValue({}); +SettingsAPI.readCategory.mockResolvedValue({ + data: { + SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL: + 'https://towerhost/sso/complete/google-oauth2/', + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'mock key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '$encrypted$', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [ + 'example.com', + 'example_2.com', + ], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: {}, + }, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + }, +}); + describe('', () => { let wrapper; - beforeEach(() => { - wrapper = mountWithContexts(); - }); + let history; + afterEach(() => { wrapper.unmount(); + jest.clearAllMocks(); }); + + beforeEach(async () => { + history = createMemoryHistory({ + initialEntries: ['/settings/google_oauth2/edit'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { router: { history } }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + test('initially renders without crashing', () => { expect(wrapper.find('GoogleOAuth2Edit').length).toBe(1); }); + + test('should display expected form fields', async () => { + expect(wrapper.find('FormGroup[label="Google OAuth2 Key"]').length).toBe(1); + expect(wrapper.find('FormGroup[label="Google OAuth2 Secret"]').length).toBe( + 1 + ); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Allowed Domains"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Extra Arguments"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Organization Map"]').length + ).toBe(1); + expect( + wrapper.find('FormGroup[label="Google OAuth2 Team Map"]').length + ).toBe(1); + }); + + test('should successfully send default values to api on form revert all', async () => { + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + expect(wrapper.find('RevertAllAlert')).toHaveLength(0); + await act(async () => { + wrapper + .find('button[aria-label="Revert all to default"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(wrapper.find('RevertAllAlert')).toHaveLength(1); + await act(async () => { + wrapper + .find('RevertAllAlert button[aria-label="Confirm revert all"]') + .invoke('onClick')(); + }); + wrapper.update(); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: null, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: null, + }); + }); + + test('should successfully send request to api on form submission', async () => { + act(() => { + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper + .find( + 'FormGroup[fieldId="SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET"] button[aria-label="Revert"]' + ) + .invoke('onClick')(); + wrapper.find('input#SOCIAL_AUTH_GOOGLE_OAUTH2_KEY').simulate('change', { + target: { value: 'new key', name: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY' }, + }); + wrapper + .find('CodeMirrorInput#SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP') + .invoke('onChange')('{\n"Default":{\n"users":\nfalse\n}\n}'); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledWith({ + SOCIAL_AUTH_GOOGLE_OAUTH2_KEY: 'new key', + SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: '', + SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: [], + SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP: {}, + SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP: { + Default: { + users: false, + }, + }, + }); + }); + + test('should navigate to Google OAuth 2.0 detail on successful submission', async () => { + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + expect(history.location.pathname).toEqual( + '/settings/google_oauth2/details' + ); + }); + + test('should navigate to Google OAuth 2.0 detail when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); + }); + expect(history.location.pathname).toEqual( + '/settings/google_oauth2/details' + ); + }); + + test('should display error message on unsuccessful submission', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error)); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0); + await act(async () => { + wrapper.find('Form').invoke('onSubmit')(); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1); + }); + + test('should display ContentError on throw', async () => { + SettingsAPI.readCategory.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + expect(wrapper.find('ContentError').length).toBe(1); + }); }); From daeba1a8980659eb13c75de057f206089d714983 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 10 Dec 2020 14:39:25 -0500 Subject: [PATCH 03/55] output timing data for isolated playbook runs * We batch logging isolated management playbook output. This results in the timestamp of the log being useless when trying to determine when each task in the playbook ran. * To fix this, we enable timestamp logging at the playbook level via ansible `profile_tasks` callback plugin. --- awx/main/isolated/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 1c0978f432..9a45db6f49 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -123,6 +123,7 @@ class IsolatedManager(object): dir=private_data_dir ) params = self.runner_params.copy() + params.get('envvars', dict())['ANSIBLE_CALLBACK_WHITELIST'] = 'profile_tasks' params['playbook'] = playbook params['private_data_dir'] = iso_dir if idle_timeout: From d39d4d9a9eaab39a8c3ae22dc2a877e684f82b0d Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 10 Dec 2020 14:42:46 -0500 Subject: [PATCH 04/55] add job id to iso management playbook output * It's hard/impossible to know what job a check_isolated.yml playbook runs for by just looking at the logs. * Forward the job id for which an iso management playbook is running for and output that job id so it can be found in the logs. --- awx/main/isolated/manager.py | 8 ++++++-- awx/playbooks/check_isolated.yml | 3 +++ awx/playbooks/run_isolated.yml | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 9a45db6f49..4f8f2ce6bb 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -169,7 +169,8 @@ class IsolatedManager(object): extravars = { 'src': self.private_data_dir, 'dest': settings.AWX_PROOT_BASE_PATH, - 'ident': self.ident + 'ident': self.ident, + 'job_id': self.instance.id, } if playbook: extravars['playbook'] = playbook @@ -205,7 +206,10 @@ class IsolatedManager(object): :param interval: an interval (in seconds) to wait between status polls """ interval = interval if interval is not None else settings.AWX_ISOLATED_CHECK_INTERVAL - extravars = {'src': self.private_data_dir} + extravars = { + 'src': self.private_data_dir, + 'job_id': self.instance.id + } status = 'failed' rc = None last_check = time.time() diff --git a/awx/playbooks/check_isolated.yml b/awx/playbooks/check_isolated.yml index 18b3305846..472b772fbb 100644 --- a/awx/playbooks/check_isolated.yml +++ b/awx/playbooks/check_isolated.yml @@ -9,6 +9,9 @@ - ansible.posix tasks: + - name: "Output job the playbook is running for" + debug: + msg: "Checking on job {{ job_id }}" - name: Determine if daemon process is alive. shell: "ansible-runner is-alive {{src}}" diff --git a/awx/playbooks/run_isolated.yml b/awx/playbooks/run_isolated.yml index 4e3b7b54ee..76ea42d17c 100644 --- a/awx/playbooks/run_isolated.yml +++ b/awx/playbooks/run_isolated.yml @@ -13,6 +13,10 @@ - ansible.posix tasks: + - name: "Output job the playbook is running for" + debug: + msg: "Checking on job {{ job_id }}" + - name: synchronize job environment with isolated host synchronize: copy_links: true From 566913fceccb2ccb87f5811171e5b95f0400396c Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 10 Dec 2020 14:44:48 -0500 Subject: [PATCH 05/55] log time it took to run check_isolated.yml * Knowing how long check_isolated.yml ran can be helpful in debuging the isolated execution path. Especially if you suspect the connection speed or reliability of the control node -> execution node --- awx/main/isolated/manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 4f8f2ce6bb..de4783e277 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -7,6 +7,7 @@ import tempfile import time import logging import yaml +import datetime from django.conf import settings import ansible_runner @@ -225,9 +226,13 @@ class IsolatedManager(object): logger.warning('Isolated job {} was manually canceled.'.format(self.instance.id)) logger.debug('Checking on isolated job {} with `check_isolated.yml`.'.format(self.instance.id)) + time_start = datetime.datetime.now() runner_obj = self.run_management_playbook('check_isolated.yml', self.private_data_dir, extravars=extravars) + time_end = datetime.datetime.now() + time_diff = time_end - time_start + logger.debug('Finished checking on isolated job {} with `check_isolated.yml` took {} seconds.'.format(self.instance.id, time_diff.total_seconds())) status, rc = runner_obj.status, runner_obj.rc if self.check_callback is not None and not self.captured_command_artifact: From 32ad6cdea63859cd749f1e33aed2573ede819e65 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 10 Dec 2020 15:02:33 -0500 Subject: [PATCH 06/55] enable iso logger * The namespace for isolated logging was not enabled. Add a handler and logger so that it's enabled. This is particularly useful when the logging level is switched to DEBUG --- awx/settings/defaults.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d47277eaed..0ea3722e13 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -932,6 +932,14 @@ LOGGING = { 'backupCount': 5, 'formatter':'simple', }, + 'isolated_manager': { + 'level': 'WARNING', + 'class':'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(LOG_ROOT, 'isolated_manager.log'), + 'maxBytes': 1024 * 1024 * 5, # 5 MB + 'backupCount': 5, + 'formatter':'simple', + }, }, 'loggers': { 'django': { @@ -981,6 +989,11 @@ LOGGING = { 'awx.main.wsbroadcast': { 'handlers': ['wsbroadcast'], }, + 'awx.isolated.manager': { + 'level': 'WARNING', + 'handlers': ['console', 'file', 'isolated_manager'], + 'propagate': True + }, 'awx.isolated.manager.playbooks': { 'handlers': ['management_playbooks'], 'propagate': False From fc2a2e538f0e756fa69effae83ed46822466fbc0 Mon Sep 17 00:00:00 2001 From: Will Haines Date: Thu, 10 Dec 2020 18:01:20 -0700 Subject: [PATCH 07/55] Enabled jinja2.ChainableUndefined for custom webhook notifications Signed-off-by: Will Haines --- awx/main/models/notifications.py | 4 ++-- requirements/requirements_ansible.in | 2 +- requirements/requirements_ansible.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 11d97c7690..33562e7fca 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -12,7 +12,7 @@ from django.core.mail.message import EmailMessage from django.db import connection from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_str, force_text -from jinja2 import sandbox +from jinja2 import sandbox, ChainableUndefined from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError # AWX @@ -429,7 +429,7 @@ class JobNotificationMixin(object): raise RuntimeError("Define me") def build_notification_message(self, nt, status): - env = sandbox.ImmutableSandboxedEnvironment() + env = sandbox.ImmutableSandboxedEnvironment(undefined=ChainableUndefined) from awx.api.serializers import UnifiedJobSerializer job_serialization = UnifiedJobSerializer(self).to_representation(self) diff --git a/requirements/requirements_ansible.in b/requirements/requirements_ansible.in index ec3c2984f5..c36e22bcb6 100644 --- a/requirements/requirements_ansible.in +++ b/requirements/requirements_ansible.in @@ -43,7 +43,7 @@ azure-mgmt-iothub==0.7.0 # AWS boto==2.47.0 # last which does not break ec2 scripts boto3==1.9.223 -jinja2==2.10.1 # required for native jinja2 types for inventory compat mode +jinja2==2.11.2 # required for ChainableUndefined # netconf for network modules ncclient==0.6.3 # netaddr filter diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 05355fb8b8..cf945c462f 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -58,7 +58,7 @@ idna==2.8 # via requests ipaddress==1.0.23; python_version < "3" # via cryptography, kubernetes, openstacksdk iso8601==0.1.12 # via keystoneauth1, openstacksdk isodate==0.6.0 # via msrest -jinja2==2.10.1 # via -r /awx_devel/requirements/requirements_ansible.in, openshift +jinja2==2.11.2 # via -r /awx_devel/requirements/requirements_ansible.in, openshift jmespath==0.9.4 # via azure-cli-core, boto3, botocore, knack, openstacksdk jsonpatch==1.24 # via openstacksdk jsonpointer==2.0 # via jsonpatch From 06fa2a9e26c98067b3b82790f2c3788696a1a778 Mon Sep 17 00:00:00 2001 From: VGU Date: Sun, 13 Dec 2020 18:56:20 +0100 Subject: [PATCH 08/55] Add test_openstack_client_config_generation_with_project_region_name test --- .../plugins/openstack/files/file_reference | 1 + awx/main/tests/unit/test_tasks.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference index c578942ca1..601878072d 100644 --- a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference +++ b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference @@ -9,3 +9,4 @@ clouds: username: fooo private: true verify: false + region_name: fooo diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index f94c70c739..37759a1f36 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -246,6 +246,52 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou } +@pytest.mark.parametrize("source,expected", [ + (None, True), (False, False), (True, True) +]) +def test_openstack_client_config_generation_with_project_region_name(mocker, source, expected, private_data_dir): + update = tasks.RunInventoryUpdate() + credential_type = CredentialType.defaults['openstack']() + inputs = { + 'host': 'https://keystone.openstack.example.org', + 'username': 'demo', + 'password': 'secrete', + 'project': 'demo-project', + 'domain': 'my-demo-domain', + 'project_domain_name': 'project-domain', + 'project_region_name': 'region-name', + } + if source is not None: + inputs['verify_ssl'] = source + credential = Credential(pk=1, credential_type=credential_type, inputs=inputs) + + inventory_update = mocker.Mock(**{ + 'source': 'openstack', + 'source_vars_dict': {}, + 'get_cloud_credential': mocker.Mock(return_value=credential), + 'get_extra_credentials': lambda x: [], + 'ansible_virtualenv_path': '/venv/foo' + }) + cloud_config = update.build_private_data(inventory_update, private_data_dir) + cloud_credential = yaml.safe_load( + cloud_config.get('credentials')[credential] + ) + assert cloud_credential['clouds'] == { + 'devstack': { + 'auth': { + 'auth_url': 'https://keystone.openstack.example.org', + 'password': 'secrete', + 'project_name': 'demo-project', + 'username': 'demo', + 'domain_name': 'my-demo-domain', + 'project_domain_name': 'project-domain', + }, + 'verify': expected, + 'private': True, + 'region_name': 'region-name', + } + } + @pytest.mark.parametrize("source,expected", [ (False, False), (True, True) ]) From bfb00aecbe18d7b3aeadcda03dd28ff8223fdf50 Mon Sep 17 00:00:00 2001 From: VGU Date: Sun, 13 Dec 2020 18:57:13 +0100 Subject: [PATCH 09/55] Add project_region_name input --- awx/main/models/credential/__init__.py | 5 +++++ awx/main/models/credential/injectors.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 66db962430..e8a2884083 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -819,6 +819,11 @@ ManagedCredentialType( 'It is only needed for Keystone v3 authentication ' 'URLs. Refer to Ansible Tower documentation for ' 'common scenarios.') + }, { + 'id': 'region', + 'label': ugettext_noop('Region Name'), + 'type': 'string', + 'help_text': ugettext_noop('For some cloud providers, like OVH, region must be specified'), }, { 'id': 'verify_ssl', 'label': ugettext_noop('Verify SSL'), diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 75d1f17bfe..d7f64b70a9 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -82,14 +82,30 @@ def _openstack_data(cred): if cred.has_input('domain'): openstack_auth['domain_name'] = cred.get_input('domain', default='') verify_state = cred.get_input('verify_ssl', default=True) + +# if cred.has_input('project_region_name'): +# openstack_data = { +# 'clouds': { +# 'devstack': { +# 'auth': openstack_auth, +# 'verify': verify_state, +# 'region_name': cred.get_input('project_region_name', default='') +# }, +# }, +# } +# else: openstack_data = { 'clouds': { 'devstack': { 'auth': openstack_auth, - 'verify': verify_state, + 'verify': verify_state }, }, } + + if cred.has_input('project_region_name'): + openstack_data['clouds']['devstack']['region_name'] = cred.get_input('project_region_name', default='') + return openstack_data From e35f1afd57e51f50315ed4b67fa1154cbab6c08b Mon Sep 17 00:00:00 2001 From: VGU Date: Sun, 13 Dec 2020 19:05:29 +0100 Subject: [PATCH 10/55] Fix lint --- awx/main/models/credential/injectors.py | 11 ----------- awx/main/tests/unit/test_tasks.py | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index d7f64b70a9..73cd018393 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -83,17 +83,6 @@ def _openstack_data(cred): openstack_auth['domain_name'] = cred.get_input('domain', default='') verify_state = cred.get_input('verify_ssl', default=True) -# if cred.has_input('project_region_name'): -# openstack_data = { -# 'clouds': { -# 'devstack': { -# 'auth': openstack_auth, -# 'verify': verify_state, -# 'region_name': cred.get_input('project_region_name', default='') -# }, -# }, -# } -# else: openstack_data = { 'clouds': { 'devstack': { diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 37759a1f36..0a530bef51 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -292,6 +292,7 @@ def test_openstack_client_config_generation_with_project_region_name(mocker, sou } } + @pytest.mark.parametrize("source,expected", [ (False, False), (True, True) ]) From 8153d60a5ff86b5bfc62d4cd461dc554add9a68a Mon Sep 17 00:00:00 2001 From: VGU Date: Sun, 13 Dec 2020 20:15:12 +0100 Subject: [PATCH 11/55] Rollback to origin file --- awx/main/models/credential/injectors.py | 2 +- .../tests/data/inventory/plugins/openstack/files/file_reference | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 73cd018393..ef30b91945 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -87,7 +87,7 @@ def _openstack_data(cred): 'clouds': { 'devstack': { 'auth': openstack_auth, - 'verify': verify_state + 'verify': verify_state, }, }, } diff --git a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference index 601878072d..c578942ca1 100644 --- a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference +++ b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference @@ -9,4 +9,3 @@ clouds: username: fooo private: true verify: false - region_name: fooo From 14f2803ea7b8b82e63ea377c7bf3ac3584034d21 Mon Sep 17 00:00:00 2001 From: VGU Date: Sun, 13 Dec 2020 20:20:47 +0100 Subject: [PATCH 12/55] Add 'Region Name' label for openstack credential --- awx/locale/django.pot | 9 +++++++++ awx/locale/en-us/LC_MESSAGES/django.po | 9 +++++++++ awx/locale/fr/LC_MESSAGES/django.po | 10 ++++++++++ .../Credential/shared/data.credentialTypes.json | 5 +++++ 4 files changed, 33 insertions(+) diff --git a/awx/locale/django.pot b/awx/locale/django.pot index 3d2cf41999..e5fbe05390 100644 --- a/awx/locale/django.pot +++ b/awx/locale/django.pot @@ -3354,6 +3354,15 @@ msgid "" "common scenarios." msgstr "" +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" + #: awx/main/models/credential/__init__.py:824 #: awx/main/models/credential/__init__.py:1131 #: awx/main/models/credential/__init__.py:1166 diff --git a/awx/locale/en-us/LC_MESSAGES/django.po b/awx/locale/en-us/LC_MESSAGES/django.po index 3d2cf41999..e5fbe05390 100644 --- a/awx/locale/en-us/LC_MESSAGES/django.po +++ b/awx/locale/en-us/LC_MESSAGES/django.po @@ -3354,6 +3354,15 @@ msgid "" "common scenarios." msgstr "" +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" + #: awx/main/models/credential/__init__.py:824 #: awx/main/models/credential/__init__.py:1131 #: awx/main/models/credential/__init__.py:1166 diff --git a/awx/locale/fr/LC_MESSAGES/django.po b/awx/locale/fr/LC_MESSAGES/django.po index 62c2ba7292..bcb54c548b 100644 --- a/awx/locale/fr/LC_MESSAGES/django.po +++ b/awx/locale/fr/LC_MESSAGES/django.po @@ -3294,6 +3294,16 @@ msgid "" "common scenarios." msgstr "Les domaines OpenStack définissent les limites administratives. Ils sont nécessaires uniquement pour les URL d’authentification Keystone v3. Voir la documentation Ansible Tower pour les scénarios courants." +#: awx/main/models/credential/__init__.py:824 +msgid "Region Name" +msgstr "Nom de la region" + +#: awx/main/models/credential/__init__.py:826 +msgid "" +"For some cloud providers, like OVH, region must be specified." +msgstr "" +"Chez certains fournisseurs, comme OVH, vous devez spécifier le nom de la région" + #: awx/main/models/credential/__init__.py:812 #: awx/main/models/credential/__init__.py:1110 #: awx/main/models/credential/__init__.py:1144 diff --git a/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json b/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json index b7b3189951..6281f15024 100644 --- a/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json +++ b/awx/ui_next/src/screens/Credential/shared/data.credentialTypes.json @@ -275,6 +275,11 @@ "type": "string", "help_text": "OpenStack domains define administrative boundaries. It is only needed for Keystone v3 authentication URLs. Refer to Ansible Tower documentation for common scenarios." }, + { + "id": "project_region_name", + "label": "Region Name", + "type": "string" + }, { "id": "verify_ssl", "label": "Verify SSL", From 8ceb5059772d0d7063a0ff1fc0c1e0734e76bb77 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 26 Dec 2020 13:01:25 -0500 Subject: [PATCH 13/55] Add standalone target for rendering official Dockerfile With the next commit it will be possible to run: ``` $ make Dockerfile $ docker build . ``` --- Makefile | 3 +++ installer/roles/image_build/templates/Dockerfile.j2 | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 783547930f..415abd7104 100644 --- a/Makefile +++ b/Makefile @@ -622,3 +622,6 @@ psql-container: VERSION: @echo "awx: $(VERSION)" + +Dockerfile: installer/roles/image_build/templates/Dockerfile.j2 + ansible localhost -m template -a "src=installer/roles/image_build/templates/Dockerfile.j2 dest=Dockerfile" diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index 64417060c7..89e7d543d3 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -1,9 +1,11 @@ -{% if build_dev|bool %} +{% if build_dev|default(False)|bool %} ### This file is generated from ### installer/roles/image_build/templates/Dockerfile.j2 ### ### DO NOT EDIT ### +{% else %} + {% set build_dev = False %} {% endif %} # Locations - set globally to be used across stages From ab6430e50d83de31e092b4fdf2a7ff4c4f3d2c20 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 26 Dec 2020 13:06:29 -0500 Subject: [PATCH 14/55] Dramatically simplify image_build role This does a few things: - Removes need for awx_sdist_builder image - Reorders Dockerfile steps to optimize image cache between prod and dev builds - Unifies VENV_BASE and COLLECTION_BASE in prod and dev builds --- .dockerignore | 1 - Makefile | 3 +- awx/settings/development.py | 2 +- awx/settings/local_settings.py.docker_compose | 2 +- .../roles/image_build/files/Dockerfile.sdist | 22 ----- installer/roles/image_build/tasks/main.yml | 98 ++----------------- .../roles/image_build/templates/Dockerfile.j2 | 79 ++++++++------- 7 files changed, 51 insertions(+), 156 deletions(-) delete mode 100644 installer/roles/image_build/files/Dockerfile.sdist diff --git a/.dockerignore b/.dockerignore index f5faf1f0e3..46c83b0467 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1 @@ -.git awx/ui/node_modules diff --git a/Makefile b/Makefile index 415abd7104..0b74a6fc17 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ PYCURL_SSL_LIBRARY ?= openssl COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_HOST ?= $(shell hostname) -VENV_BASE ?= /venv +VENV_BASE ?= /var/lib/awx/venv/ +COLLECTION_BASE ?= /var/lib/awx/vendor/awx_ansible_collections SCL_PREFIX ?= CELERY_SCHEDULE_FILE ?= /var/lib/awx/beat.db diff --git a/awx/settings/development.py b/awx/settings/development.py index 108767b98c..db58f42245 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -148,7 +148,7 @@ include(optional('/etc/tower/settings.py'), scope=locals()) include(optional('/etc/tower/conf.d/*.py'), scope=locals()) # Installed differently in Dockerfile compared to production versions -AWX_ANSIBLE_COLLECTIONS_PATHS = '/vendor/awx_ansible_collections' +AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' BASE_VENV_PATH = "/venv/" ANSIBLE_VENV_PATH = os.path.join(BASE_VENV_PATH, "ansible") diff --git a/awx/settings/local_settings.py.docker_compose b/awx/settings/local_settings.py.docker_compose index 213f4efe4b..f853f35e12 100644 --- a/awx/settings/local_settings.py.docker_compose +++ b/awx/settings/local_settings.py.docker_compose @@ -53,7 +53,7 @@ if "pytest" in sys.modules: PROJECTS_ROOT = '/var/lib/awx/projects/' # Location for cross-development of inventory plugins -AWX_ANSIBLE_COLLECTIONS_PATHS = '/vendor/awx_ansible_collections' +AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' # Absolute filesystem path to the directory for job status stdout # This directory should not be web-accessible diff --git a/installer/roles/image_build/files/Dockerfile.sdist b/installer/roles/image_build/files/Dockerfile.sdist deleted file mode 100644 index c4ed45477f..0000000000 --- a/installer/roles/image_build/files/Dockerfile.sdist +++ /dev/null @@ -1,22 +0,0 @@ -FROM centos:8 - -RUN dnf -y update && dnf -y install epel-release && \ - dnf install -y bzip2 \ - gcc-c++ \ - gettext \ - git \ - make \ - nodejs \ - python3 \ - python3-setuptools - -# Use the distro provided npm to bootstrap our required version of node -RUN npm install -g n && n 14.15.1 && dnf remove -y nodejs - -RUN mkdir -p /.npm && chmod g+rwx /.npm - -ENV PATH=/usr/local/n/versions/node/14.15.1/bin:$PATH - -WORKDIR "/awx" - -CMD ["make", "sdist"] diff --git a/installer/roles/image_build/tasks/main.yml b/installer/roles/image_build/tasks/main.yml index 46add2552c..d14530b7a2 100644 --- a/installer/roles/image_build/tasks/main.yml +++ b/installer/roles/image_build/tasks/main.yml @@ -19,76 +19,6 @@ delegate_to: localhost when: awx_official|default(false)|bool -- name: Set sdist file name - set_fact: - awx_sdist_file: "awx-{{ awx_version }}.tar.gz" - -- name: AWX Distribution - debug: - msg: "{{ awx_sdist_file }}" - -- name: Stat distribution file - stat: - path: "../dist/{{ awx_sdist_file }}" - delegate_to: localhost - register: sdist - -- name: Clean distribution - command: make clean - args: - chdir: .. - ignore_errors: true - when: not sdist.stat.exists - delegate_to: localhost - -- name: Build sdist builder image - docker_image: - build: - path: "{{ role_path }}/files" - dockerfile: Dockerfile.sdist - pull: false - args: - http_proxy: "{{ http_proxy | default('') }}" - https_proxy: "{{ https_proxy | default('') }}" - no_proxy: "{{ no_proxy | default('') }}" - name: awx_sdist_builder - tag: "{{ awx_version }}" - source: 'build' - force_source: true - delegate_to: localhost - when: use_container_for_build|default(true)|bool - -- name: Get current uid - command: id -u - register: uid - -- name: Build AWX distribution using container - docker_container: - env: - http_proxy: "{{ http_proxy | default('') }}" - https_proxy: "{{ https_proxy | default('') }}" - no_proxy: "{{ no_proxy | default('') }}" - image: "awx_sdist_builder:{{ awx_version }}" - name: awx_sdist_builder - state: started - user: "{{ uid.stdout }}" - detach: false - volumes: - - ../:/awx:Z - delegate_to: localhost - when: use_container_for_build|default(true)|bool - -- name: Build AWX distribution locally - command: make sdist - args: - chdir: .. - delegate_to: localhost - when: not use_container_for_build|default(true)|bool - -- name: Set docker build base path - set_fact: - docker_base_path: "{{ awx_local_base_config_path|default('/tmp') }}/docker-image" - - name: Set awx image name set_fact: awx_image: "{{ awx_image|default('awx') }}" @@ -97,32 +27,16 @@ template: src: Dockerfile.j2 dest: ../Dockerfile - -- name: Build base awx image - docker_image: - build: - path: ".." - dockerfile: Dockerfile - pull: false - args: - http_proxy: "{{ http_proxy | default('') }}" - https_proxy: "{{ https_proxy | default('') }}" - no_proxy: "{{ no_proxy | default('') }}" - name: "{{ awx_image }}" - tag: "{{ awx_version }}" - source: 'build' - force_source: true delegate_to: localhost +# Calling Docker directly because docker-py doesnt support BuildKit +- name: Build AWX image + command: docker build -t {{ awx_image }}:{{ awx_version }} .. + delegate_to: localhost + when: use_container_for_build|default(true)|bool + - name: Tag awx images as latest command: "docker tag {{ item }}:{{ awx_version }} {{ item }}:latest" delegate_to: localhost with_items: - "{{ awx_image }}" - -- name: Clean docker base directory - file: - path: "{{ docker_base_path }}" - state: absent - when: cleanup_docker_base|default(True)|bool - delegate_to: localhost diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index 89e7d543d3..ebbd4f885e 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -9,15 +9,11 @@ {% endif %} # Locations - set globally to be used across stages -ARG VENV_BASE="{% if not build_dev|bool %}/var/lib/awx{% endif %}/venv" -ARG COLLECTION_BASE="{% if not build_dev|bool %}/var/lib/awx{% endif %}/vendor/awx_ansible_collections" +ARG COLLECTION_BASE="/var/lib/awx/vendor/awx_ansible_collections" # Build container FROM centos:8 as builder -ARG VENV_BASE -ARG COLLECTION_BASE - ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 @@ -74,16 +70,21 @@ RUN cd /tmp && make requirements_collections ADD requirements/requirements_dev.txt /tmp/requirements RUN cd /tmp && make requirements_awx_dev requirements_ansible_dev {% endif %} + {% if not build_dev|bool %} -COPY dist/{{ awx_sdist_file }} /tmp/{{ awx_sdist_file }} -RUN mkdir -p -m 755 /var/lib/awx && \ - OFFICIAL=yes /var/lib/awx/venv/awx/bin/pip install /tmp/{{ awx_sdist_file }} +# Use the distro provided npm to bootstrap our required version of node +RUN npm install -g n && n 14.15.1 && dnf remove -y nodejs + +# Copy source into builder, build sdist, install it into awx venv +COPY . /tmp/src/ +WORKDIR /tmp/src/ +RUN make sdist && \ + /var/lib/awx/venv/awx/bin/pip install dist/awx-$(cat VERSION).tar.gz {% endif %} # Final container(s) FROM centos:8 -ARG VENV_BASE ARG COLLECTION_BASE ENV LANG en_US.UTF-8 @@ -92,28 +93,6 @@ ENV LC_ALL en_US.UTF-8 USER root -{% if build_dev|bool %} -# Install development/test requirements -RUN dnf -y install \ - gtk3 \ - gettext \ - alsa-lib \ - libX11-xcb \ - libXScrnSaver \ - strace \ - vim \ - nmap-ncat \ - nodejs \ - nss \ - make \ - patch \ - tmux \ - wget \ - diffutils \ - unzip && \ - npm install -g n && n 14.15.1 && dnf remove -y nodejs -{% endif %} - # Install runtime requirements RUN dnf -y update && \ dnf -y install epel-release 'dnf-command(config-manager)' && \ @@ -165,16 +144,40 @@ RUN cd /usr/local/bin && \ curl -L https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz | \ tar -xz --strip-components=1 --wildcards --no-anchored 'oc' +{% if build_dev|bool %} +# Install development/test requirements +RUN dnf --enablerepo=debuginfo -y install \ + gdb \ + gtk3 \ + gettext \ + alsa-lib \ + libX11-xcb \ + libXScrnSaver \ + strace \ + vim \ + nmap-ncat \ + nodejs \ + nss \ + make \ + patch \ + python3-debuginfo \ + socat \ + tmux \ + wget \ + diffutils \ + unzip && \ + npm install -g n && n 14.15.1 && dnf remove -y nodejs +{% endif %} + # Copy app from builder +COPY --from=builder /var/lib/awx /var/lib/awx + {%if build_dev|bool %} -COPY --from=builder /venv /venv -COPY --from=builder /vendor /vendor RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/nginx/nginx.csr \ -subj "/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost" && \ openssl x509 -req -days 365 -in /etc/nginx/nginx.csr -signkey /etc/nginx/nginx.key -out /etc/nginx/nginx.crt && \ chmod 640 /etc/nginx/nginx.{csr,key,crt} {% else %} -COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {% endif %} @@ -223,17 +226,17 @@ RUN chmod u+s /usr/bin/bwrap ; \ {% if build_dev|bool %} RUN for dir in \ - /venv \ - /venv/awx/lib/python3.6 \ + /var/lib/awx/venv \ + /var/lib/awx/venv/awx/lib/python3.6 \ /var/lib/awx/projects \ /var/lib/awx/rsyslog \ /var/run/awx-rsyslog \ /.ansible \ - /vendor ; \ + /var/lib/awx/vendor ; \ do mkdir -m 0775 -p $dir ; chmod g+rw $dir ; chgrp root $dir ; done && \ for file in \ /var/run/nginx.pid \ - /venv/awx/lib/python3.6/site-packages/awx.egg-link ; \ + /var/lib/awx/venv/awx/lib/python3.6/site-packages/awx.egg-link ; \ do touch $file ; chmod g+rw $file ; done {% endif %} From 1033c4d25125859f9ec20e111b8b26b7ba4af860 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 26 Dec 2020 13:10:28 -0500 Subject: [PATCH 15/55] Explicitly run image_build and image_push on localhost --- installer/build.yml | 2 +- installer/roles/image_build/tasks/main.yml | 5 ----- installer/roles/image_push/tasks/main.yml | 3 --- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/installer/build.yml b/installer/build.yml index 8ef6f2b1ce..0bea5821e3 100644 --- a/installer/build.yml +++ b/installer/build.yml @@ -1,6 +1,6 @@ --- - name: Build AWX Docker Images - hosts: all + hosts: localhost gather_facts: true roles: - {role: image_build} diff --git a/installer/roles/image_build/tasks/main.yml b/installer/roles/image_build/tasks/main.yml index d14530b7a2..e2c73719b4 100644 --- a/installer/roles/image_build/tasks/main.yml +++ b/installer/roles/image_build/tasks/main.yml @@ -7,7 +7,6 @@ - name: Verify awx-logos directory exists for official install stat: path: "../../awx-logos" - delegate_to: localhost register: logosdir failed_when: logosdir.stat.isdir is not defined or not logosdir.stat.isdir when: awx_official|default(false)|bool @@ -16,7 +15,6 @@ copy: src: "../../awx-logos/awx/ui/client/assets/" dest: "../awx/ui_next/public/static/media/" - delegate_to: localhost when: awx_official|default(false)|bool - name: Set awx image name @@ -27,16 +25,13 @@ template: src: Dockerfile.j2 dest: ../Dockerfile - delegate_to: localhost # Calling Docker directly because docker-py doesnt support BuildKit - name: Build AWX image command: docker build -t {{ awx_image }}:{{ awx_version }} .. - delegate_to: localhost when: use_container_for_build|default(true)|bool - name: Tag awx images as latest command: "docker tag {{ item }}:{{ awx_version }} {{ item }}:latest" - delegate_to: localhost with_items: - "{{ awx_image }}" diff --git a/installer/roles/image_push/tasks/main.yml b/installer/roles/image_push/tasks/main.yml index e005af1096..9561af8ac8 100644 --- a/installer/roles/image_push/tasks/main.yml +++ b/installer/roles/image_push/tasks/main.yml @@ -6,7 +6,6 @@ password: "{{ docker_registry_password }}" reauthorize: true when: docker_registry is defined and docker_registry_password is defined - delegate_to: localhost - name: Remove local images to ensure proper push behavior block: @@ -15,7 +14,6 @@ name: "{{ docker_registry }}/{{ docker_registry_repository }}/{{ awx_image }}" tag: "{{ awx_version }}" state: absent - delegate_to: localhost - name: Tag and Push Container Images block: @@ -28,7 +26,6 @@ with_items: - "latest" - "{{ awx_version }}" - delegate_to: localhost - name: Set full image path for Registry set_fact: From 6f9862c72e98dc2f0e77bbeae997190201cf2efb Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Sat, 26 Dec 2020 21:29:22 -0500 Subject: [PATCH 16/55] Sweeping replace of old dev venv paths --- Makefile | 4 ++-- .../management/commands/inventory_import.py | 2 +- awx/main/tests/functional/models/test_job.py | 18 +++++++++--------- awx/main/tests/unit/test_tasks.py | 10 +++++----- awx/settings/development.py | 2 +- .../AnsibleSelect/AnsibleSelect.test.jsx | 4 ++-- .../src/screens/Host/data.hostFacts.json | 10 +++++----- .../InventorySourceDetail.test.jsx | 2 +- .../Inventory/shared/InventorySourceForm.jsx | 2 +- .../Inventory/shared/data.hostFacts.json | 10 +++++----- .../shared/data.inventory_source.json | 2 +- .../src/screens/Job/shared/data.job.json | 14 +++++++------- .../OrganizationAdd/OrganizationAdd.test.jsx | 2 +- .../Organization/shared/OrganizationForm.jsx | 2 +- .../shared/OrganizationForm.test.jsx | 2 +- .../Project/ProjectAdd/ProjectAdd.test.jsx | 2 +- .../Project/ProjectEdit/ProjectEdit.test.jsx | 2 +- .../src/screens/Project/shared/ProjectForm.jsx | 4 ++-- .../Project/shared/ProjectForm.test.jsx | 2 +- .../plugins/modules/tower_project.py | 2 +- .../test/awx/test_inventory_source.py | 6 +++--- pytest.ini | 4 ++-- tools/scripts/awx-python | 2 +- 23 files changed, 55 insertions(+), 55 deletions(-) diff --git a/Makefile b/Makefile index 0b74a6fc17..54bbf2175f 100644 --- a/Makefile +++ b/Makefile @@ -271,7 +271,7 @@ uwsgi: collectstatic @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/venv/awx --chdir=/awx_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --lazy-apps --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" --hook-accepting1="exec:supervisorctl restart tower-processes:awx-dispatcher tower-processes:awx-receiver" + uwsgi -b 32768 --socket 127.0.0.1:8050 --module=awx.wsgi:application --home=/var/lib/awx/venv/awx --chdir=/awx_devel/ --vacuum --processes=5 --harakiri=120 --master --no-orphans --py-autoreload 1 --max-requests=1000 --stats /tmp/stats.socket --lazy-apps --logformat "%(addr) %(method) %(uri) - %(proto) %(status)" --hook-accepting1="exec:supervisorctl restart tower-processes:awx-dispatcher tower-processes:awx-receiver" daphne: @if [ "$(VENV_BASE)" ]; then \ @@ -341,7 +341,7 @@ check: flake8 pep8 # pyflakes pylint awx-link: [ -d "/awx_devel/awx.egg-info" ] || python3 /awx_devel/setup.py egg_info_dev - cp -f /tmp/awx.egg-link /venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link + cp -f /tmp/awx.egg-link /var/lib/awx/venv/awx/lib/python$(PYTHON_VERSION)/site-packages/awx.egg-link TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 30529cdf72..a86cc3db48 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -133,7 +133,7 @@ class AnsibleInventoryLoader(object): # NOTE: why do we add "python" to the start of these args? # the script that runs ansible-inventory specifies a python interpreter # that makes no sense in light of the fact that we put all the dependencies - # inside of /venv/ansible, so we override the specified interpreter + # inside of /var/lib/awx/venv/ansible, so we override the specified interpreter # https://github.com/ansible/ansible/issues/50714 bargs = ['python', ansible_inventory_path, '-i', self.source] bargs.extend(['--playbook-dir', functioning_dir(self.source)]) diff --git a/awx/main/tests/functional/models/test_job.py b/awx/main/tests/functional/models/test_job.py index ac8912506f..c6c4d2d6e6 100644 --- a/awx/main/tests/functional/models/test_job.py +++ b/awx/main/tests/functional/models/test_job.py @@ -16,7 +16,7 @@ def test_awx_virtualenv_from_settings(inventory, project, machine_credential): ) jt.credentials.add(machine_credential) job = jt.create_unified_job() - assert job.ansible_virtualenv_path == '/venv/ansible' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/ansible' @pytest.mark.django_db @@ -43,28 +43,28 @@ def test_awx_custom_virtualenv(inventory, project, machine_credential, organizat jt.credentials.add(machine_credential) job = jt.create_unified_job() - job.organization.custom_virtualenv = '/venv/fancy-org' + job.organization.custom_virtualenv = '/var/lib/awx/venv/fancy-org' job.organization.save() - assert job.ansible_virtualenv_path == '/venv/fancy-org' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-org' - job.project.custom_virtualenv = '/venv/fancy-proj' + job.project.custom_virtualenv = '/var/lib/awx/venv/fancy-proj' job.project.save() - assert job.ansible_virtualenv_path == '/venv/fancy-proj' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-proj' - job.job_template.custom_virtualenv = '/venv/fancy-jt' + job.job_template.custom_virtualenv = '/var/lib/awx/venv/fancy-jt' job.job_template.save() - assert job.ansible_virtualenv_path == '/venv/fancy-jt' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-jt' @pytest.mark.django_db def test_awx_custom_virtualenv_without_jt(project): - project.custom_virtualenv = '/venv/fancy-proj' + project.custom_virtualenv = '/var/lib/awx/venv/fancy-proj' project.save() job = Job(project=project) job.save() job = Job.objects.get(pk=job.id) - assert job.ansible_virtualenv_path == '/venv/fancy-proj' + assert job.ansible_virtualenv_path == '/var/lib/awx/venv/fancy-proj' @pytest.mark.django_db diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index f94c70c739..b1c7765328 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -180,7 +180,7 @@ def test_openstack_client_config_generation(mocker, source, expected, private_da 'source_vars_dict': {}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], - 'ansible_virtualenv_path': '/venv/foo' + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) cloud_credential = yaml.safe_load( @@ -224,7 +224,7 @@ def test_openstack_client_config_generation_with_project_domain_name(mocker, sou 'source_vars_dict': {}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], - 'ansible_virtualenv_path': '/venv/foo' + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) cloud_credential = yaml.safe_load( @@ -267,7 +267,7 @@ def test_openstack_client_config_generation_with_private_source_vars(mocker, sou 'source_vars_dict': {'private': source}, 'get_cloud_credential': mocker.Mock(return_value=credential), 'get_extra_credentials': lambda x: [], - 'ansible_virtualenv_path': '/venv/foo' + 'ansible_virtualenv_path': '/var/lib/awx/venv/foo' }) cloud_config = update.build_private_data(inventory_update, private_data_dir) cloud_credential = yaml.load( @@ -625,13 +625,13 @@ class TestGenericRun(): def test_invalid_custom_virtualenv(self, patch_Job, private_data_dir): job = Job(project=Project(), inventory=Inventory()) - job.project.custom_virtualenv = '/venv/missing' + job.project.custom_virtualenv = '/var/lib/awx/venv/missing' task = tasks.RunJob() with pytest.raises(tasks.InvalidVirtualenvError) as e: task.build_env(job, private_data_dir) - assert 'Invalid virtual environment selected: /venv/missing' == str(e.value) + assert 'Invalid virtual environment selected: /var/lib/awx/venv/missing' == str(e.value) class TestAdhocRun(TestJobExecution): diff --git a/awx/settings/development.py b/awx/settings/development.py index db58f42245..9846705fa5 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -150,7 +150,7 @@ include(optional('/etc/tower/conf.d/*.py'), scope=locals()) # Installed differently in Dockerfile compared to production versions AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' -BASE_VENV_PATH = "/venv/" +BASE_VENV_PATH = "/var/lib/awx/venv/" ANSIBLE_VENV_PATH = os.path.join(BASE_VENV_PATH, "ansible") AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") diff --git a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx index d9bd7c669d..ced058754a 100644 --- a/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx +++ b/awx/ui_next/src/components/AnsibleSelect/AnsibleSelect.test.jsx @@ -6,12 +6,12 @@ const mockData = [ { key: 'baz', label: 'Baz', - value: '/venv/baz/', + value: '/var/lib/awx/venv/baz/', }, { key: 'default', label: 'Default', - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', }, ]; diff --git a/awx/ui_next/src/screens/Host/data.hostFacts.json b/awx/ui_next/src/screens/Host/data.hostFacts.json index a8427e0003..2507d267e3 100644 --- a/awx/ui_next/src/screens/Host/data.hostFacts.json +++ b/awx/ui_next/src/screens/Host/data.hostFacts.json @@ -83,7 +83,7 @@ "PWD": "/tmp/awx_13_r1ffeqze/project", "HOME": "/var/lib/awx", "LANG": "\"en-us\"", - "PATH": "/venv/ansible/bin:/venv/awx/bin:/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH": "/var/lib/awx/venv/ansible/bin:/var/lib/awx/venv/awx/bin:/var/lib/awx/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "SHLVL": "4", "JOB_ID": "13", "LC_ALL": "en_US.UTF-8", @@ -96,9 +96,9 @@ "SDB_PORT": "7899", "MAKEFLAGS": "w", "MAKELEVEL": "2", - "PYTHONPATH": "/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "PYTHONPATH": "/var/lib/awx/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "CURRENT_UID": "501", - "VIRTUAL_ENV": "/venv/ansible", + "VIRTUAL_ENV": "/var/lib/awx/venv/ansible", "INVENTORY_ID": "1", "MAX_EVENT_RES": "700000", "PROOT_TMP_DIR": "/tmp", @@ -106,7 +106,7 @@ "SDB_NOTIFY_HOST": "docker.for.mac.host.internal", "AWX_GROUP_QUEUES": "tower", "PROJECT_REVISION": "9e2cd25bfb26ba82f40cf31276e1942bf38b3a30", - "ANSIBLE_VENV_PATH": "/venv/ansible", + "ANSIBLE_VENV_PATH": "/var/lib/awx/venv/ansible", "ANSIBLE_ROLES_PATH": "/tmp/awx_13_r1ffeqze/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles", "RUNNER_OMIT_EVENTS": "False", "SUPERVISOR_ENABLED": "1", @@ -119,7 +119,7 @@ "DJANGO_SETTINGS_MODULE": "awx.settings.development", "ANSIBLE_STDOUT_CALLBACK": "awx_display", "SUPERVISOR_PROCESS_NAME": "awx-dispatcher", - "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "ANSIBLE_COLLECTIONS_PATHS": "/tmp/awx_13_r1ffeqze/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections", "ANSIBLE_HOST_KEY_CHECKING": "False", "RUNNER_ONLY_FAILED_EVENTS": "False", diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx index 1071b91cc3..50a0f13f67 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx @@ -58,7 +58,7 @@ describe('InventorySourceDetail', () => { assertDetail(wrapper, 'Description', 'mock description'); assertDetail(wrapper, 'Source', 'Sourced from a Project'); assertDetail(wrapper, 'Organization', 'Mock Org'); - assertDetail(wrapper, 'Ansible environment', '/venv/custom'); + assertDetail(wrapper, 'Ansible environment', '/var/lib/awx/venv/custom'); assertDetail(wrapper, 'Project', 'Mock Project'); assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Verbosity', '2 (Debug)'); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx index f1286e9a2b..2b1cff9115 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx @@ -55,7 +55,7 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => { const [venvField] = useField('custom_virtualenv'); const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', key: 'default', }; diff --git a/awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json b/awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json index a8427e0003..2507d267e3 100644 --- a/awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json +++ b/awx/ui_next/src/screens/Inventory/shared/data.hostFacts.json @@ -83,7 +83,7 @@ "PWD": "/tmp/awx_13_r1ffeqze/project", "HOME": "/var/lib/awx", "LANG": "\"en-us\"", - "PATH": "/venv/ansible/bin:/venv/awx/bin:/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH": "/var/lib/awx/venv/ansible/bin:/var/lib/awx/venv/awx/bin:/var/lib/awx/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "SHLVL": "4", "JOB_ID": "13", "LC_ALL": "en_US.UTF-8", @@ -96,9 +96,9 @@ "SDB_PORT": "7899", "MAKEFLAGS": "w", "MAKELEVEL": "2", - "PYTHONPATH": "/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "PYTHONPATH": "/var/lib/awx/venv/ansible/lib/python3.6/site-packages:/awx_devel/awx/lib:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "CURRENT_UID": "501", - "VIRTUAL_ENV": "/venv/ansible", + "VIRTUAL_ENV": "/var/lib/awx/venv/ansible", "INVENTORY_ID": "1", "MAX_EVENT_RES": "700000", "PROOT_TMP_DIR": "/tmp", @@ -106,7 +106,7 @@ "SDB_NOTIFY_HOST": "docker.for.mac.host.internal", "AWX_GROUP_QUEUES": "tower", "PROJECT_REVISION": "9e2cd25bfb26ba82f40cf31276e1942bf38b3a30", - "ANSIBLE_VENV_PATH": "/venv/ansible", + "ANSIBLE_VENV_PATH": "/var/lib/awx/venv/ansible", "ANSIBLE_ROLES_PATH": "/tmp/awx_13_r1ffeqze/requirements_roles:~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles", "RUNNER_OMIT_EVENTS": "False", "SUPERVISOR_ENABLED": "1", @@ -119,7 +119,7 @@ "DJANGO_SETTINGS_MODULE": "awx.settings.development", "ANSIBLE_STDOUT_CALLBACK": "awx_display", "SUPERVISOR_PROCESS_NAME": "awx-dispatcher", - "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", + "ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback:/var/lib/awx/venv/awx/lib/python3.6/site-packages/ansible_runner/callbacks", "ANSIBLE_COLLECTIONS_PATHS": "/tmp/awx_13_r1ffeqze/requirements_collections:~/.ansible/collections:/usr/share/ansible/collections", "ANSIBLE_HOST_KEY_CHECKING": "False", "RUNNER_ONLY_FAILED_EVENTS": "False", diff --git a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json index ad1e313611..550cb8138e 100644 --- a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json +++ b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json @@ -98,7 +98,7 @@ "credential": 8, "overwrite":true, "overwrite_vars":true, - "custom_virtualenv":"/venv/custom", + "custom_virtualenv":"/var/lib/awx/venv/custom", "timeout":0, "verbosity":2, "last_job_run":null, diff --git a/awx/ui_next/src/screens/Job/shared/data.job.json b/awx/ui_next/src/screens/Job/shared/data.job.json index 8bebbbda67..98d071c876 100644 --- a/awx/ui_next/src/screens/Job/shared/data.job.json +++ b/awx/ui_next/src/screens/Job/shared/data.job.json @@ -114,7 +114,7 @@ "started": "2019-08-08T19:24:18.329589Z", "finished": "2019-08-08T19:24:50.119995Z", "elapsed": 31.79, - "job_args": "[\"bwrap\", \"--unshare-pid\", \"--dev-bind\", \"/\", \"/\", \"--proc\", \"/proc\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpvsg8ly2y\", \"/etc/ssh\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq_grmdym\", \"/projects\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpfq8ea2z6\", \"/tmp\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq6v4y_tt\", \"/var/lib/awx\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpupj_jhhb\", \"/var/log\", \"--ro-bind\", \"/venv/ansible\", \"/venv/ansible\", \"--ro-bind\", \"/venv/awx\", \"/venv/awx\", \"--bind\", \"/projects/_6__demo_project\", \"/projects/_6__demo_project\", \"--bind\", \"/tmp/awx_2_a4b1afiw\", \"/tmp/awx_2_a4b1afiw\", \"--chdir\", \"/projects/_6__demo_project\", \"ansible-playbook\", \"-u\", \"admin\", \"-i\", \"/tmp/awx_2_a4b1afiw/tmppb57i4_e\", \"-e\", \"@/tmp/awx_2_a4b1afiw/env/extravars\", \"chatty_tasks.yml\"]", + "job_args": "[\"bwrap\", \"--unshare-pid\", \"--dev-bind\", \"/\", \"/\", \"--proc\", \"/proc\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpvsg8ly2y\", \"/etc/ssh\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq_grmdym\", \"/projects\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpfq8ea2z6\", \"/tmp\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq6v4y_tt\", \"/var/lib/awx\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpupj_jhhb\", \"/var/log\", \"--ro-bind\", \"/var/lib/awx/venv/ansible\", \"/var/lib/awx/venv/ansible\", \"--ro-bind\", \"/var/lib/awx/venv/awx\", \"/var/lib/awx/venv/awx\", \"--bind\", \"/projects/_6__demo_project\", \"/projects/_6__demo_project\", \"--bind\", \"/tmp/awx_2_a4b1afiw\", \"/tmp/awx_2_a4b1afiw\", \"--chdir\", \"/projects/_6__demo_project\", \"ansible-playbook\", \"-u\", \"admin\", \"-i\", \"/tmp/awx_2_a4b1afiw/tmppb57i4_e\", \"-e\", \"@/tmp/awx_2_a4b1afiw/env/extravars\", \"chatty_tasks.yml\"]", "job_cwd": "/projects/_6__demo_project", "job_env": { "HOSTNAME": "awx", @@ -123,9 +123,9 @@ "LC_ALL": "en_US.UTF-8", "SDB_HOST": "0.0.0.0", "MAKELEVEL": "2", - "VIRTUAL_ENV": "/venv/ansible", + "VIRTUAL_ENV": "/var/lib/awx/venv/ansible", "MFLAGS": "-w", - "PATH": "/venv/ansible/bin:/venv/awx/bin:/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH": "/var/lib/awx/venv/ansible/bin:/var/lib/awx/venv/awx/bin:/var/lib/awx/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "SUPERVISOR_GROUP_NAME": "tower-processes", "PWD": "/awx_devel", "LANG": "\"en-us\"", @@ -138,7 +138,7 @@ "SUPERVISOR_SERVER_URL": "unix:///tmp/supervisor.sock", "SUPERVISOR_PROCESS_NAME": "awx-dispatcher", "CURRENT_UID": "501", - "_": "/venv/awx/bin/python3", + "_": "/var/lib/awx/venv/awx/bin/python3", "DJANGO_SETTINGS_MODULE": "awx.settings.development", "DJANGO_LIVE_TEST_SERVER_ADDRESS": "localhost:9013-9199", "SDB_NOTIFY_HOST": "docker.for.mac.host.internal", @@ -147,11 +147,11 @@ "ANSIBLE_HOST_KEY_CHECKING": "False", "ANSIBLE_INVENTORY_UNPARSED_FAILED": "True", "ANSIBLE_PARAMIKO_RECORD_HOST_KEYS": "False", - "ANSIBLE_VENV_PATH": "/venv/ansible", + "ANSIBLE_VENV_PATH": "/var/lib/awx/venv/ansible", "PROOT_TMP_DIR": "/tmp", "AWX_PRIVATE_DATA_DIR": "/tmp/awx_2_a4b1afiw", "ANSIBLE_COLLECTIONS_PATHS": "/tmp/collections", - "PYTHONPATH": "/venv/ansible/lib/python2.7/site-packages:/awx_devel/awx/lib:", + "PYTHONPATH": "/var/lib/awx/venv/ansible/lib/python2.7/site-packages:/awx_devel/awx/lib:", "JOB_ID": "2", "INVENTORY_ID": "1", "PROJECT_REVISION": "23f070aad8e2da131d97ea98b42b553ccf0b0b82", @@ -184,5 +184,5 @@ "play_count": 1, "task_count": 1 }, - "custom_virtualenv": "/venv/ansible" + "custom_virtualenv": "/var/lib/awx/venv/ansible" } diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx index ff969b86b5..8fa4e2cbc2 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx @@ -153,7 +153,7 @@ describe('', () => { .find('FormSelectOption') .first() .prop('value') - ).toEqual('/venv/ansible/'); + ).toEqual('/var/lib/awx/venv/ansible/'); }); test('AnsibleSelect component does not render if there are 0 virtual environments', async () => { diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index c78b178943..094e6ac5b6 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -31,7 +31,7 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { const defaultVenv = { label: i18n._(t`Use Default Ansible Environment`), - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', key: 'default', }; const { custom_virtualenvs } = useContext(ConfigContext); diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx index 004c7d1577..67cf0a60d6 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx @@ -200,7 +200,7 @@ describe('', () => { .find('FormSelectOption') .first() .prop('value') - ).toEqual('/venv/ansible/'); + ).toEqual('/var/lib/awx/venv/ansible/'); }); test('onSubmit associates and disassociates instance groups', async () => { diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index e6141bebb7..8bc136b889 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -24,7 +24,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', }; const projectOptionsResolve = { diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx index dc1a49eb78..1a62a3f2f0 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -25,7 +25,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', summary_fields: { credential: { id: 100, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index fe0c8b1bb2..c5b454246f 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -284,11 +284,11 @@ function ProjectFormFields({ data={[ { label: i18n._(t`Use Default Ansible Environment`), - value: '/venv/ansible/', + value: '/var/lib/awx/venv/ansible/', key: 'default', }, ...custom_virtualenvs - .filter(datum => datum !== '/venv/ansible/') + .filter(datum => datum !== '/var/lib/awx/venv/ansible/') .map(datum => ({ label: datum, value: datum, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx index 03defe391a..7e88bc7f10 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx @@ -22,7 +22,7 @@ describe('', () => { scm_update_on_launch: true, scm_update_cache_timeout: 3, allow_override: false, - custom_virtualenv: '/venv/custom-env', + custom_virtualenv: '/var/lib/awx/venv/custom-env', summary_fields: { credential: { id: 100, diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 3e6d6ab442..ca848e7d69 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -153,7 +153,7 @@ EXAMPLES = ''' organization: "test" scm_update_on_launch: True scm_update_cache_timeout: 60 - custom_virtualenv: "/var/lib/awx/venv/ansible-2.2" + custom_virtualenv: "/var/lib/awx/var/lib/awx/venv/ansible-2.2" state: present tower_config_file: "~/tower_cli.cfg" ''' diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index 1bced2eb67..3e65feaddb 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -133,10 +133,10 @@ def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker, proje inventory=base_inventory, source_project=project, source='scm', - custom_virtualenv='/venv/foobar/' + custom_virtualenv='/var/lib/awx/venv/foobar/' ) # mock needed due to API behavior, not incorrect client behavior - with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=['/venv/foobar/']): + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=['/var/lib/awx/venv/foobar/']): result = run_module('tower_inventory_source', dict( name='foo', description='this is the changed description', @@ -148,7 +148,7 @@ def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker, proje ), admin_user) assert result.pop('changed', None), result inv_src.refresh_from_db() - assert inv_src.custom_virtualenv == '/venv/foobar/' + assert inv_src.custom_virtualenv == '/var/lib/awx/venv/foobar/' assert inv_src.description == 'this is the changed description' diff --git a/pytest.ini b/pytest.ini index ff89dc85f3..fc407b5f17 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] DJANGO_SETTINGS_MODULE = awx.settings.development -python_paths = /venv/tower/lib/python3.6/site-packages -site_dirs = /venv/tower/lib/python3.6/site-packages +python_paths = /var/lib/awx/venv/tower/lib/python3.6/site-packages +site_dirs = /var/lib/awx/venv/tower/lib/python3.6/site-packages python_files = *.py addopts = --reuse-db --nomigrations --tb=native markers = diff --git a/tools/scripts/awx-python b/tools/scripts/awx-python index f2116c574c..00d5e7363e 100755 --- a/tools/scripts/awx-python +++ b/tools/scripts/awx-python @@ -9,7 +9,7 @@ for scl in rh-postgresql10; do done # Enable Tower virtualenv -for venv_path in /var/lib/awx/venv/awx /venv/awx; do +for venv_path in /var/lib/awx/venv/awx; do if [ -f $venv_path/bin/activate ]; then . $venv_path/bin/activate fi From b857fb507409199b6a596a3cd63c1bae439f8826 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 29 Dec 2020 19:14:05 -0500 Subject: [PATCH 17/55] Remove use_container_for_build from inventory --- installer/inventory | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/installer/inventory b/installer/inventory index 89d0684a70..d4596f5d96 100644 --- a/installer/inventory +++ b/installer/inventory @@ -101,17 +101,6 @@ pg_port=5432 # containerized postgres deployment on OpenShift # pg_admin_password=postgrespass -# Use a local distribution build container image for building the AWX package -# This is helpful if you don't want to bother installing the build-time dependencies as -# it is taken care of already. -# NOTE: IMPORTANT: If you are running a mininshift install, using this container might not work -# if you are using certain drivers like KVM where the source tree can't be mapped -# into the build container. -# Thus this setting must be set to False which will trigger a local build. To view the -# typical dependencies that you might need to install see: -# installer/image_build/files/Dockerfile.sdist -# use_container_for_build=true - # This will create or update a default admin (superuser) account in AWX, if not provided # then these default values are used admin_user=admin From 642e6f792cad6365506d2ae1f48f2a828b290fb8 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 29 Dec 2020 19:14:16 -0500 Subject: [PATCH 18/55] Remove unnecessary conditional from image build task --- installer/roles/image_build/tasks/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/installer/roles/image_build/tasks/main.yml b/installer/roles/image_build/tasks/main.yml index e2c73719b4..463e12ec73 100644 --- a/installer/roles/image_build/tasks/main.yml +++ b/installer/roles/image_build/tasks/main.yml @@ -29,7 +29,6 @@ # Calling Docker directly because docker-py doesnt support BuildKit - name: Build AWX image command: docker build -t {{ awx_image }}:{{ awx_version }} .. - when: use_container_for_build|default(true)|bool - name: Tag awx images as latest command: "docker tag {{ item }}:{{ awx_version }} {{ item }}:latest" From e611a67be76733d63f59ca8d2f3d2931f7076c24 Mon Sep 17 00:00:00 2001 From: David Roble Date: Mon, 4 Jan 2021 11:21:30 -0500 Subject: [PATCH 19/55] Fixed link to Content Hub in Tower module documentation Signed-off-by: David Roble --- .../tools/roles/template_galaxy/templates/README.md.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 index 8a5743d34f..ed02006c3d 100644 --- a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 +++ b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 @@ -31,7 +31,7 @@ with the current AWX version, for example: `awx_collection/awx-awx-9.2.0.tar.gz` Installing the `tar.gz` involves no special instructions. {% else %} -This collection should be installed from [Content Hub][https://cloud.redhat.com/ansible/automation-hub/ansible/tower/] +This collection should be installed from [Content Hub](https://cloud.redhat.com/ansible/automation-hub/ansible/tower/) {% endif %} ## Running From 4ea757c91a6127f07a3ed65429841484df786be8 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 23 Nov 2020 14:38:36 -0500 Subject: [PATCH 20/55] Move jt/wfjt created/modified details to the end right before the full width details --- .../JobTemplateDetail/JobTemplateDetail.jsx | 20 +++++++------- .../WorkflowJobTemplateDetail.jsx | 26 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index e25032e527..8f3fe4ffcd 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -219,16 +219,6 @@ function JobTemplateDetail({ i18n, template }) { value={verbosityDetails[0].details} /> - - )} + + {summary_fields.credentials && summary_fields.credentials.length > 0 && ( )} - {renderOptionsField && ( - - )} )} + {renderOptionsField && ( + + )} + + {summary_fields.labels?.results?.length > 0 && ( - - {summary_fields.user_capabilities && From 1dbadca78eadc2e74f7d46e7df8539e4dcc95031 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 14 Dec 2020 14:41:03 -0500 Subject: [PATCH 21/55] Adds searchable keys and related keys --- .../components/Lookup/CredentialLookup.jsx | 20 ++++++++++------ .../CredentialList/CredentialList.jsx | 23 ++++++++++++++++++- .../ContainerGroupEdit.test.jsx | 2 +- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index 2b398abcbe..df9964440b 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { arrayOf, bool, @@ -8,7 +9,6 @@ import { string, oneOfType, } from 'prop-types'; -import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { FormGroup } from '@patternfly/react-core'; @@ -39,13 +39,13 @@ function CredentialLookup({ credentialTypeKind, credentialTypeNamespace, value, - history, i18n, tooltip, isDisabled, autoPopulate, multiple, }) { + const history = useHistory(); const autoPopulateLookup = useAutoPopulateLookup(onChange); const { result: { count, credentials, relatedSearchableKeys, searchableKeys }, @@ -72,22 +72,28 @@ function CredentialLookup({ ...typeNamespaceParams, }) ), - CredentialsAPI.readOptions, + CredentialsAPI.readOptions(), ]); if (autoPopulate) { autoPopulateLookup(data.results); } + const searchKeys = Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable); + const item = searchKeys.indexOf('type'); + if (item) { + searchKeys[item] = 'credential_type__kind'; + } + return { count: data.count, credentials: data.results, relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), - searchableKeys: Object.keys( - actionsResponse.data?.actions?.GET || {} - ).filter(key => actionsResponse.data?.actions?.GET[key]?.filterable), + searchableKeys: searchKeys, }; }, [ autoPopulate, @@ -222,4 +228,4 @@ CredentialLookup.defaultProps = { }; export { CredentialLookup as _CredentialLookup }; -export default withI18n()(withRouter(CredentialLookup)); +export default withI18n()(CredentialLookup); diff --git a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx index 26272ee4ad..e4a10ed86c 100644 --- a/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx +++ b/awx/ui_next/src/screens/Credential/CredentialList/CredentialList.jsx @@ -26,7 +26,13 @@ function CredentialList({ i18n }) { const location = useLocation(); const { - result: { credentials, credentialCount, actions }, + result: { + credentials, + credentialCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchCredentials, @@ -37,16 +43,29 @@ function CredentialList({ i18n }) { CredentialsAPI.read(params), CredentialsAPI.readOptions(), ]); + const searchKeys = Object.keys( + credActions.data.actions?.GET || {} + ).filter(key => credActions.data.actions?.GET[key].filterable); + const item = searchKeys.indexOf('type'); + if (item) { + searchKeys[item] = 'credential_type__kind'; + } return { credentials: creds.data.results, credentialCount: creds.data.count, actions: credActions.data.actions, + relatedSearchableKeys: ( + credActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: searchKeys, }; }, [location]), { credentials: [], credentialCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -102,6 +121,8 @@ function CredentialList({ i18n }) { itemCount={credentialCount} qsConfig={QS_CONFIG} onRowClick={handleSelect} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarSearchColumns={[ { name: i18n._(t`Name`), diff --git a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx index d63996dde8..937aa15adb 100644 --- a/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/ContainerGroupEdit/ContainerGroupEdit.test.jsx @@ -123,7 +123,7 @@ describe('', () => { }); test('called InstanceGroupsAPI.readOptions', async () => { - expect(InstanceGroupsAPI.readOptions).toHaveBeenCalledTimes(1); + expect(InstanceGroupsAPI.readOptions).toHaveBeenCalled(); }); test('handleCancel returns the user to container group detail', async () => { From 0b3d9b026dafb5c4f251be986d906ed17c09e655 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 4 Jan 2021 16:17:46 -0500 Subject: [PATCH 22/55] Adds created/modified details to several areas --- .../ApplicationDetails/ApplicationDetails.jsx | 11 ++++++++++- awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx | 13 ++++++++++++- .../NotificationTemplateDetail.jsx | 13 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx index 5e55cb1611..80d7a8c916 100644 --- a/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationDetails/ApplicationDetails.jsx @@ -7,7 +7,11 @@ import { Button } from '@patternfly/react-core'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; -import { Detail, DetailList } from '../../../components/DetailList'; +import { + Detail, + DetailList, + UserDateDetail, +} from '../../../components/DetailList'; import { ApplicationsAPI } from '../../../api'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; @@ -98,6 +102,11 @@ function ApplicationDetails({ value={getClientType(application.client_type)} dataCy="app-detail-client-type" /> + + {application.summary_fields.user_capabilities && diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 9963fbbba8..9255b27af1 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -7,7 +7,11 @@ import { Button, Chip, Label } from '@patternfly/react-core'; import styled from 'styled-components'; import AlertModal from '../../../components/AlertModal'; -import { DetailList, Detail } from '../../../components/DetailList'; +import { + DetailList, + Detail, + UserDateDetail, +} from '../../../components/DetailList'; import { CardBody, CardActionsRow } from '../../../components/Card'; import ChipGroup from '../../../components/ChipGroup'; import CredentialChip from '../../../components/CredentialChip'; @@ -80,6 +84,7 @@ const getLaunchedByDetails = ({ summary_fields = {}, related = {} }) => { function JobDetail({ job, i18n }) { const { + created_by, credential, credentials, instance_group: instanceGroup, @@ -289,6 +294,12 @@ function JobDetail({ job, i18n }) { } /> )} + + {job.extra_vars && ( )} + + {hasCustomMessages(messages, typeMessageDefaults) && ( Date: Tue, 5 Jan 2021 09:13:29 -0500 Subject: [PATCH 23/55] Small docs update Small docs update. Fix broken link, and update node version used. See: https://github.com/ansible/awx/pull/8766/files --- CONTRIBUTING.md | 2 +- awx/ui_next/CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd3da38b51..0e47416e12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ If you're not using Docker for Mac, or Docker for Windows, you may need, or choo #### Frontend Development -See [the ui development documentation](awx/ui/README.md). +See [the ui development documentation](awx/ui_next/CONTRIBUTING.md). ### Build the environment diff --git a/awx/ui_next/CONTRIBUTING.md b/awx/ui_next/CONTRIBUTING.md index 575e08e913..c0a3eaefc4 100644 --- a/awx/ui_next/CONTRIBUTING.md +++ b/awx/ui_next/CONTRIBUTING.md @@ -57,7 +57,7 @@ The UI is built using [ReactJS](https://reactjs.org/docs/getting-started.html) a The AWX UI requires the following: -- Node 10.x LTS +- Node 14.x LTS - NPM 6.x LTS Run the following to install all the dependencies: From 474252dbff02acaecbfd7a515dbf7558860cfe29 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 5 Jan 2021 09:22:56 -0500 Subject: [PATCH 24/55] specify isolated_manager.log path * By default, log files are created in the dir relative to the awx source. For production, explicitly specify the log file path --- awx/settings/production.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/settings/production.py b/awx/settings/production.py index fb24b7087f..aa163af44d 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -54,6 +54,7 @@ LOGGING['handlers']['task_system']['filename'] = '/var/log/tower/task_system.log LOGGING['handlers']['management_playbooks']['filename'] = '/var/log/tower/management_playbooks.log' # noqa LOGGING['handlers']['system_tracking_migrations']['filename'] = '/var/log/tower/tower_system_tracking_migrations.log' # noqa LOGGING['handlers']['rbac_migrations']['filename'] = '/var/log/tower/tower_rbac_migrations.log' # noqa +LOGGING['handlers']['isolated_manager']['filename'] = '/var/log/tower/isolated_manager.log' # noqa # Store a snapshot of default settings at this point before loading any # customizable config files. From d79b96b6ccd2ef06d503c9201beb8b6d85b7b51a Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 5 Jan 2021 10:15:32 -0500 Subject: [PATCH 25/55] Make workflow_job.assert_successful() give specifics --- awxkit/awxkit/api/mixins/has_status.py | 14 +++++++---- awxkit/awxkit/api/pages/workflow_jobs.py | 30 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/awxkit/awxkit/api/mixins/has_status.py b/awxkit/awxkit/api/mixins/has_status.py index bd76400baa..db14874b6c 100644 --- a/awxkit/awxkit/api/mixins/has_status.py +++ b/awxkit/awxkit/api/mixins/has_status.py @@ -47,6 +47,13 @@ class HasStatus(object): def wait_until_started(self, interval=1, timeout=60): return self.wait_until_status(self.started_statuses, interval=interval, timeout=timeout) + def failure_output_details(self): + if getattr(self, 'result_stdout', ''): + output = bytes_to_str(self.result_stdout) + if output: + return '\nstdout:\n{}'.format(output) + return '' + def assert_status(self, status_list, msg=None): if isinstance(status_list, str): status_list = [status_list] @@ -65,10 +72,9 @@ class HasStatus(object): msg += '\njob_explanation: {}'.format(bytes_to_str(self.job_explanation)) if getattr(self, 'result_traceback', ''): msg += '\nresult_traceback:\n{}'.format(bytes_to_str(self.result_traceback)) - if getattr(self, 'result_stdout', ''): - output = bytes_to_str(self.result_stdout) - if output: - msg = msg + '\nstdout:\n{}'.format(output) + + msg += self.failure_output_details() + if getattr(self, 'job_explanation', '').startswith('Previous Task Failed'): try: data = json.loads(self.job_explanation.replace('Previous Task Failed: ', '')) diff --git a/awxkit/awxkit/api/pages/workflow_jobs.py b/awxkit/awxkit/api/pages/workflow_jobs.py index d7fe487030..66ff73ac1e 100644 --- a/awxkit/awxkit/api/pages/workflow_jobs.py +++ b/awxkit/awxkit/api/pages/workflow_jobs.py @@ -13,11 +13,37 @@ class WorkflowJob(UnifiedJob): result = self.related.relaunch.post(payload) return self.walk(result.url) + def failure_output_details(self): + """Special implementation of this part of assert_status so that + workflow_job.assert_successful() will give a breakdown of failure + """ + msg = '\nNode summary:' + node_list = self.related.workflow_nodes.get().results + + for node in node_list: + msg += '\n{}:'.format(node.id) + if node.job: + msg += ' {}'.format(node.summary_fields.job) + else: + msg += ' None' + for rel in ('failure_nodes', 'always_nodes', 'success_nodes'): + val = getattr(node, rel, []) + if val: + msg += ' {} {}'.format(rel, val) + + msg += '\n\nUnhandled individual job failures:\n' + for node in node_list: + if node.job and not (node.failure_nodes or node.always_nodes): + job = node.related.job.get() + try: + job.assert_successful() + except Exception as e: + msg += str(e) + return msg + @property def result_stdout(self): # workflow jobs do not have result_stdout - # which is problematic for the UnifiedJob.is_successful reliance on - # related stdout endpoint. if 'result_stdout' not in self.json: return 'Unprovided AWX field.' else: From 9a16e9f787be6fba68009462722abd569e8fb0dd Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 5 Jan 2021 10:26:38 -0500 Subject: [PATCH 26/55] Condense logic for handling null job --- awxkit/awxkit/api/pages/workflow_jobs.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/awxkit/awxkit/api/pages/workflow_jobs.py b/awxkit/awxkit/api/pages/workflow_jobs.py index 66ff73ac1e..ac3f36a08d 100644 --- a/awxkit/awxkit/api/pages/workflow_jobs.py +++ b/awxkit/awxkit/api/pages/workflow_jobs.py @@ -21,11 +21,7 @@ class WorkflowJob(UnifiedJob): node_list = self.related.workflow_nodes.get().results for node in node_list: - msg += '\n{}:'.format(node.id) - if node.job: - msg += ' {}'.format(node.summary_fields.job) - else: - msg += ' None' + msg += '\n{}: {}'.format(node.id, node.summary_fields.get('job')) for rel in ('failure_nodes', 'always_nodes', 'success_nodes'): val = getattr(node, rel, []) if val: From d3c51ce75df31d67b711268eaa355d56293b5b0f Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 5 Jan 2021 10:33:35 -0500 Subject: [PATCH 27/55] Minor organization clarity for workflow failure summary --- awxkit/awxkit/api/pages/workflow_jobs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awxkit/awxkit/api/pages/workflow_jobs.py b/awxkit/awxkit/api/pages/workflow_jobs.py index ac3f36a08d..36afc94460 100644 --- a/awxkit/awxkit/api/pages/workflow_jobs.py +++ b/awxkit/awxkit/api/pages/workflow_jobs.py @@ -17,9 +17,9 @@ class WorkflowJob(UnifiedJob): """Special implementation of this part of assert_status so that workflow_job.assert_successful() will give a breakdown of failure """ - msg = '\nNode summary:' node_list = self.related.workflow_nodes.get().results + msg = '\nNode summary:' for node in node_list: msg += '\n{}: {}'.format(node.id, node.summary_fields.get('job')) for rel in ('failure_nodes', 'always_nodes', 'success_nodes'): @@ -29,12 +29,14 @@ class WorkflowJob(UnifiedJob): msg += '\n\nUnhandled individual job failures:\n' for node in node_list: + # nodes without always or failure paths consider failures unhandled if node.job and not (node.failure_nodes or node.always_nodes): job = node.related.job.get() try: job.assert_successful() except Exception as e: msg += str(e) + return msg @property From f40ee7ca15c28c418b0bcd99bf45a271d19c6a97 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 5 Jan 2021 10:45:40 -0500 Subject: [PATCH 28/55] pin pip-tools for now a new version of pip-tools changed the format of dependency annotations in generated requirements.txt files we should probably change to the new format at some point, but maybe *after* we merge a few of our long-running branches that touch these files (otherwise, managing conflicts could be pretty hellish) --- requirements/updater.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/updater.sh b/requirements/updater.sh index 3d93b4d815..c58f1a0f62 100755 --- a/requirements/updater.sh +++ b/requirements/updater.sh @@ -21,7 +21,7 @@ _cleanup() { install_deps() { pip install pip --upgrade - pip install pip-tools + pip install "pip-tools==5.4.0" # see https://github.com/jazzband/pip-tools/pull/1237 } generate_requirements_v3() { From ad621a7da22f04e6a778f4a3a11fd383424b86cd Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 5 Jan 2021 10:46:20 -0500 Subject: [PATCH 29/55] consolidate settings and delete dead settings --- awx/settings/defaults.py | 6 +- awx/settings/local_settings.py.docker_compose | 146 ------------- awx/settings/local_settings.py.example | 192 ------------------ awx/settings/production.py | 14 -- 4 files changed, 3 insertions(+), 355 deletions(-) delete mode 100644 awx/settings/local_settings.py.example diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 51e15a5e43..05c8a42f20 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -116,7 +116,7 @@ LOGIN_URL = '/api/login/' # Absolute filesystem path to the directory to host projects (with playbooks). # This directory should not be web-accessible. -PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects') +PROJECTS_ROOT = '/var/lib/awx/projects/' # Absolute filesystem path to the directory to host collections for # running inventory imports, isolated playbooks @@ -125,10 +125,10 @@ AWX_ANSIBLE_COLLECTIONS_PATHS = os.path.join(BASE_DIR, 'vendor', 'awx_ansible_co # Absolute filesystem path to the directory for job status stdout (default for # development and tests, default for production defined in production.py). This # directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_output') +JOBOUTPUT_ROOT = '/var/lib/awx/job_status/' # Absolute filesystem path to the directory to store logs -LOG_ROOT = os.path.join(BASE_DIR) +LOG_ROOT = '/var/log/tower/' # The heartbeat file for the tower scheduler SCHEDULE_METADATA_LOCATION = os.path.join(BASE_DIR, '.tower_cycle') diff --git a/awx/settings/local_settings.py.docker_compose b/awx/settings/local_settings.py.docker_compose index f853f35e12..88ef90fd64 100644 --- a/awx/settings/local_settings.py.docker_compose +++ b/awx/settings/local_settings.py.docker_compose @@ -48,56 +48,12 @@ if "pytest" in sys.modules: } } -# Absolute filesystem path to the directory to host projects (with playbooks). -# This directory should NOT be web-accessible. -PROJECTS_ROOT = '/var/lib/awx/projects/' - # Location for cross-development of inventory plugins AWX_ANSIBLE_COLLECTIONS_PATHS = '/var/lib/awx/vendor/awx_ansible_collections' -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_status') - # The UUID of the system, for HA. SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -USE_TZ = True -TIME_ZONE = 'UTC' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -# SECURITY WARNING: keep the secret key used in production secret! -# Hardcoded values can leak through source control. Consider loading -# the secret key from an environment variable or a file instead. -SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y' - -# HTTP headers and meta keys to search to determine remote host name or IP. Add -# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a -# reverse proxy. -REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] - -# If Tower is behind a reverse proxy/load balancer, use this setting to -# whitelist the proxy IP addresses from which Tower should trust custom -# REMOTE_HOST_HEADERS header values -# REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST'] -# PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101'] -# If this setting is an empty list (the default), the headers specified by -# REMOTE_HOST_HEADERS will be trusted unconditionally') -PROXY_IP_WHITELIST = [] - -# Define additional environment variables to be passed to ansible subprocesses -#AWX_TASK_ENV['FOO'] = 'BAR' - # If set, use -vvv for project updates instead of -v for more output. # PROJECT_UPDATE_VVV=True @@ -108,40 +64,6 @@ PROXY_IP_WHITELIST = [] # Enable logging to syslog. Setting level to ERROR captures 500 errors, # WARNING also logs 4xx responses. -LOGGING['handlers']['syslog'] = { - 'level': 'WARNING', - 'filters': ['require_debug_false'], - 'class': 'logging.NullHandler', - 'formatter': 'simple', -} - -LOGGING['loggers']['django.request']['handlers'] = ['console'] -LOGGING['loggers']['rest_framework.request']['handlers'] = ['console'] -LOGGING['loggers']['awx']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['awx.main.commands.run_callback_receiver']['handlers'] = [] # propogates to awx -LOGGING['loggers']['awx.main.tasks']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['awx.main.scheduler']['handlers'] = ['console', 'external_logger'] -LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] -LOGGING['loggers']['social']['handlers'] = ['console'] -LOGGING['loggers']['system_tracking_migrations']['handlers'] = ['console'] -LOGGING['loggers']['rbac_migrations']['handlers'] = ['console'] -LOGGING['loggers']['awx.isolated.manager.playbooks']['handlers'] = ['console'] -LOGGING['handlers']['callback_receiver'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['fact_receiver'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['task_system'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['tower_warnings'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['rbac_migrations'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['system_tracking_migrations'] = {'class': 'logging.NullHandler'} -LOGGING['handlers']['management_playbooks'] = {'class': 'logging.NullHandler'} - - -# Enable the following lines to also log to a file. -#LOGGING['handlers']['file'] = { -# 'class': 'logging.FileHandler', -# 'filename': os.path.join(BASE_DIR, 'awx.log'), -# 'formatter': 'simple', -#} - # Enable the following lines to turn on lots of permissions-related logging. #LOGGING['loggers']['awx.main.access']['level'] = 'DEBUG' #LOGGING['loggers']['awx.main.signals']['level'] = 'DEBUG' @@ -154,74 +76,6 @@ LOGGING['handlers']['management_playbooks'] = {'class': 'logging.NullHandler'} #LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] #LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG' -############################################################################### -# SCM TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of project support for -# SCM updates. The test repositories listed do not have to contain any valid -# playbooks. - -try: - path = os.path.expanduser(os.path.expandvars('~/.ssh/id_rsa')) - TEST_SSH_KEY_DATA = open(path, 'rb').read() -except IOError: - TEST_SSH_KEY_DATA = '' - -TEST_GIT_USERNAME = '' -TEST_GIT_PASSWORD = '' -TEST_GIT_KEY_DATA = TEST_SSH_KEY_DATA -TEST_GIT_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com.git' -TEST_GIT_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs.git' -TEST_GIT_PRIVATE_SSH = 'git@github.com:ansible/product-docs.git' - -TEST_SVN_USERNAME = '' -TEST_SVN_PASSWORD = '' -TEST_SVN_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com' -TEST_SVN_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs' - -# To test repo access via SSH login to localhost. -import getpass -try: - TEST_SSH_LOOPBACK_USERNAME = getpass.getuser() -except KeyError: - TEST_SSH_LOOPBACK_USERNAME = 'root' -TEST_SSH_LOOPBACK_PASSWORD = '' - -############################################################################### -# INVENTORY IMPORT TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of inventory import -# from cloud providers. - -# EC2 credentials -TEST_AWS_ACCESS_KEY_ID = '' -TEST_AWS_SECRET_ACCESS_KEY = '' -TEST_AWS_REGIONS = 'all' -# Check IAM STS credentials -TEST_AWS_SECURITY_TOKEN = '' - -# Rackspace credentials -TEST_RACKSPACE_USERNAME = '' -TEST_RACKSPACE_API_KEY = '' -TEST_RACKSPACE_REGIONS = 'all' - -# VMware credentials -TEST_VMWARE_HOST = '' -TEST_VMWARE_USER = '' -TEST_VMWARE_PASSWORD = '' - -# OpenStack credentials -TEST_OPENSTACK_HOST = '' -TEST_OPENSTACK_USER = '' -TEST_OPENSTACK_PASSWORD = '' -TEST_OPENSTACK_PROJECT = '' - -# Azure credentials. -TEST_AZURE_USERNAME = '' -TEST_AZURE_KEY_DATA = '' - BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖' BROADCAST_WEBSOCKET_PORT = 8013 BROADCAST_WEBSOCKET_VERIFY_CERT = False diff --git a/awx/settings/local_settings.py.example b/awx/settings/local_settings.py.example deleted file mode 100644 index 59f3bdfa6a..0000000000 --- a/awx/settings/local_settings.py.example +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. (formerly AnsibleWorks, Inc.) -# All Rights Reserved. - -# Local Django settings for AWX project. Rename to "local_settings.py" and -# edit as needed for your development environment. - -# All variables defined in awx/settings/development.py will already be loaded -# into the global namespace before this file is loaded, to allow for reading -# and updating the default settings as needed. - -############################################################################### -# MISC PROJECT SETTINGS -############################################################################### - -# Database settings to use PostgreSQL for development. -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'awx-dev', - 'USER': 'awx-dev', - 'PASSWORD': 'AWXsome1', - 'HOST': 'localhost', - 'PORT': '', - } -} - -# Use SQLite for unit tests instead of PostgreSQL. If the lines below are -# commented out, Django will create the test_awx-dev database in PostgreSQL to -# run unit tests. -if is_testing(sys.argv): - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'awx.sqlite3'), - 'TEST': { - # Test database cannot be :memory: for tests. - 'NAME': os.path.join(BASE_DIR, 'awx_test.sqlite3'), - }, - } - } - -# AMQP configuration. -BROKER_URL = 'amqp://guest:guest@localhost:5672' - -# Absolute filesystem path to the directory to host projects (with playbooks). -# This directory should NOT be web-accessible. -PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects') - -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = os.path.join(BASE_DIR, 'job_status') - -# The UUID of the system, for HA. -SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = None - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -# SECURITY WARNING: keep the secret key used in production secret! -# Hardcoded values can leak through source control. Consider loading -# the secret key from an environment variable or a file instead. -SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y' - -# HTTP headers and meta keys to search to determine remote host name or IP. Add -# additional items to this list, such as "HTTP_X_FORWARDED_FOR", if behind a -# reverse proxy. -REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] - -# If Tower is behind a reverse proxy/load balancer, use this setting to -# whitelist the proxy IP addresses from which Tower should trust custom -# REMOTE_HOST_HEADERS header values -# REMOTE_HOST_HEADERS = ['HTTP_X_FORWARDED_FOR', ''REMOTE_ADDR', 'REMOTE_HOST'] -# PROXY_IP_WHITELIST = ['10.0.1.100', '10.0.1.101'] -# If this setting is an empty list (the default), the headers specified by -# REMOTE_HOST_HEADERS will be trusted unconditionally') -PROXY_IP_WHITELIST = [] - -# Define additional environment variables to be passed to ansible subprocesses -#AWX_TASK_ENV['FOO'] = 'BAR' - -# If set, use -vvv for project updates instead of -v for more output. -# PROJECT_UPDATE_VVV=True - -############################################################################### -# LOGGING SETTINGS -############################################################################### - -# Enable logging to syslog. Setting level to ERROR captures 500 errors, -# WARNING also logs 4xx responses. -LOGGING['handlers']['syslog'] = { - 'level': 'WARNING', - 'filters': [], - 'class': 'logging.handlers.SysLogHandler', - 'address': '/dev/log', - 'facility': 'local0', - 'formatter': 'simple', -} - -# Enable the following lines to also log to a file. -#LOGGING['handlers']['file'] = { -# 'class': 'logging.FileHandler', -# 'filename': os.path.join(BASE_DIR, 'awx.log'), -# 'formatter': 'simple', -#} - -# Enable the following lines to turn on lots of permissions-related logging. -#LOGGING['loggers']['awx.main.access']['level'] = 'DEBUG' -#LOGGING['loggers']['awx.main.signals']['level'] = 'DEBUG' -#LOGGING['loggers']['awx.main.permissions']['level'] = 'DEBUG' - -# Enable the following line to turn on database settings logging. -#LOGGING['loggers']['awx.conf']['level'] = 'DEBUG' - -# Enable the following lines to turn on LDAP auth logging. -#LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] -#LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG' - -############################################################################### -# SCM TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of project support for -# SCM updates. The test repositories listed do not have to contain any valid -# playbooks. - -try: - path = os.path.expanduser(os.path.expandvars('~/.ssh/id_rsa')) - TEST_SSH_KEY_DATA = file(path, 'rb').read() -except IOError: - TEST_SSH_KEY_DATA = '' - -TEST_GIT_USERNAME = '' -TEST_GIT_PASSWORD = '' -TEST_GIT_KEY_DATA = TEST_SSH_KEY_DATA -TEST_GIT_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com.git' -TEST_GIT_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs.git' -TEST_GIT_PRIVATE_SSH = 'git@github.com:ansible/product-docs.git' - -TEST_SVN_USERNAME = '' -TEST_SVN_PASSWORD = '' -TEST_SVN_PUBLIC_HTTPS = 'https://github.com/ansible/ansible.github.com' -TEST_SVN_PRIVATE_HTTPS = 'https://github.com/ansible/product-docs' - -# To test repo access via SSH login to localhost. -import getpass -TEST_SSH_LOOPBACK_USERNAME = getpass.getuser() -TEST_SSH_LOOPBACK_PASSWORD = '' - -############################################################################### -# INVENTORY IMPORT TEST SETTINGS -############################################################################### - -# Define these variables to enable more complete testing of inventory import -# from cloud providers. - -# EC2 credentials -TEST_AWS_ACCESS_KEY_ID = '' -TEST_AWS_SECRET_ACCESS_KEY = '' -TEST_AWS_REGIONS = 'all' -# Check IAM STS credentials -TEST_AWS_SECURITY_TOKEN = '' - - -# Rackspace credentials -TEST_RACKSPACE_USERNAME = '' -TEST_RACKSPACE_API_KEY = '' -TEST_RACKSPACE_REGIONS = 'all' - -# VMware credentials -TEST_VMWARE_HOST = '' -TEST_VMWARE_USER = '' -TEST_VMWARE_PASSWORD = '' - -# OpenStack credentials -TEST_OPENSTACK_HOST = '' -TEST_OPENSTACK_USER = '' -TEST_OPENSTACK_PASSWORD = '' -TEST_OPENSTACK_PROJECT = '' - -# Azure credentials. -TEST_AZURE_USERNAME = '' -TEST_AZURE_KEY_DATA = '' diff --git a/awx/settings/production.py b/awx/settings/production.py index aa163af44d..02681265e6 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -30,10 +30,6 @@ SECRET_KEY = None # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = [] -# Absolute filesystem path to the directory for job status stdout -# This directory should not be web-accessible -JOBOUTPUT_ROOT = '/var/lib/awx/job_status/' - # The heartbeat file for the tower scheduler SCHEDULE_METADATA_LOCATION = '/var/lib/awx/.tower_cycle' @@ -46,16 +42,6 @@ AWX_VENV_PATH = os.path.join(BASE_VENV_PATH, "awx") AWX_ISOLATED_USERNAME = 'awx' -LOGGING['handlers']['tower_warnings']['filename'] = '/var/log/tower/tower.log' # noqa -LOGGING['handlers']['callback_receiver']['filename'] = '/var/log/tower/callback_receiver.log' # noqa -LOGGING['handlers']['dispatcher']['filename'] = '/var/log/tower/dispatcher.log' # noqa -LOGGING['handlers']['wsbroadcast']['filename'] = '/var/log/tower/wsbroadcast.log' # noqa -LOGGING['handlers']['task_system']['filename'] = '/var/log/tower/task_system.log' # noqa -LOGGING['handlers']['management_playbooks']['filename'] = '/var/log/tower/management_playbooks.log' # noqa -LOGGING['handlers']['system_tracking_migrations']['filename'] = '/var/log/tower/tower_system_tracking_migrations.log' # noqa -LOGGING['handlers']['rbac_migrations']['filename'] = '/var/log/tower/tower_rbac_migrations.log' # noqa -LOGGING['handlers']['isolated_manager']['filename'] = '/var/log/tower/isolated_manager.log' # noqa - # Store a snapshot of default settings at this point before loading any # customizable config files. DEFAULTS_SNAPSHOT = {} From 0eff06318fb256db479e6edda6bfbb55d7f21cdb Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 5 Jan 2021 11:39:40 -0500 Subject: [PATCH 30/55] Update autobahn to address CVE-2020-35678 --- requirements/requirements.in | 1 + requirements/requirements.txt | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index c4466dedc1..d6a8987c75 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -2,6 +2,7 @@ aiohttp ansible-runner>=1.4.6 ansiconv==1.0.0 # UPGRADE BLOCKER: from 2013, consider replacing instead of upgrading asciichartpy +autobahn>=20.12.3 # CVE-2020-35678 azure-keyvault==1.1.0 # see UPGRADE BLOCKERs channels channels-redis>=3.1.0 # https://github.com/django/channels_redis/issues/212 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a9c25d307d..100f9a74fb 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,7 +7,7 @@ asciichartpy==1.5.25 # via -r /awx_devel/requirements/requirements.in asgiref==3.2.5 # via channels, channels-redis, daphne async-timeout==3.0.1 # via aiohttp, aioredis attrs==19.3.0 # via aiohttp, automat, jsonschema, service-identity, twisted -autobahn==20.3.1 # via daphne +autobahn==20.12.3 # via -r /awx_devel/requirements/requirements.in, daphne automat==20.2.0 # via twisted azure-common==1.1.25 # via azure-keyvault azure-keyvault==1.1.0 # via -r /awx_devel/requirements/requirements.in @@ -19,7 +19,7 @@ channels-redis==3.1.0 # via -r /awx_devel/requirements/requirements.in channels==2.4.0 # via -r /awx_devel/requirements/requirements.in, channels-redis chardet==3.0.4 # via aiohttp, requests constantly==15.1.0 # via twisted -cryptography==2.8 # via adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core +cryptography==3.3.1 # via adal, autobahn, azure-keyvault, pyopenssl, service-identity, social-auth-core daphne==2.4.1 # via -r /awx_devel/requirements/requirements.in, channels defusedxml==0.6.0 # via python3-openid, python3-saml, social-auth-core dictdiffer==0.8.1 # via openshift @@ -46,7 +46,7 @@ gitdb==4.0.2 # via gitpython gitpython==3.1.7 # via -r /awx_devel/requirements/requirements.in google-auth==1.11.3 # via kubernetes hiredis==1.0.1 # via aioredis -hyperlink==19.0.0 # via twisted +hyperlink==20.0.1 # via autobahn, twisted idna-ssl==1.1.0 # via aiohttp idna==2.9 # via hyperlink, idna-ssl, requests, twisted, yarl importlib-metadata==1.5.0 # via importlib-resources, irc, jsonschema @@ -107,7 +107,7 @@ ruamel.yaml.clib==0.2.0 # via ruamel.yaml ruamel.yaml==0.16.10 # via openshift schedule==0.6.0 # via -r /awx_devel/requirements/requirements.in service-identity==18.1.0 # via twisted -six==1.14.0 # via ansible-runner, automat, cryptography, django-extensions, django-pglocks, google-auth, isodate, jaraco.collections, jaraco.logging, jaraco.text, jsonschema, kubernetes, openshift, pygerduty, pyopenssl, pyrad, pyrsistent, python-dateutil, slackclient, social-auth-app-django, social-auth-core, tacacs-plus, twilio, txaio, websocket-client +six==1.14.0 # via ansible-runner, automat, cryptography, django-extensions, django-pglocks, google-auth, isodate, jaraco.collections, jaraco.logging, jaraco.text, jsonschema, kubernetes, openshift, pygerduty, pyopenssl, pyrad, pyrsistent, python-dateutil, slackclient, social-auth-app-django, social-auth-core, tacacs-plus, twilio, websocket-client slackclient==1.1.2 # via -r /awx_devel/requirements/requirements.in smmap==3.0.1 # via gitdb social-auth-app-django==3.1.0 # via -r /awx_devel/requirements/requirements.in @@ -117,7 +117,7 @@ tacacs_plus==1.0 # via -r /awx_devel/requirements/requirements.in tempora==2.1.0 # via irc, jaraco.logging twilio==6.37.0 # via -r /awx_devel/requirements/requirements.in twisted[tls]==20.3.0 # via -r /awx_devel/requirements/requirements.in, daphne -txaio==20.1.1 # via autobahn +txaio==20.12.1 # via autobahn typing-extensions==3.7.4.1 # via aiohttp urllib3==1.25.8 # via kubernetes, requests uwsgi==2.0.18 # via -r /awx_devel/requirements/requirements.in From 3e8eb7f23ed8270cb12ef3cc7f7da0204cfd3260 Mon Sep 17 00:00:00 2001 From: dluong Date: Wed, 6 Jan 2021 09:52:04 -0500 Subject: [PATCH 31/55] Changed task to job in job timeout description, fixes #9016 --- awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 805e201e13..9b9dfb539e 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -433,7 +433,7 @@ function JobTemplateForm({ min="0" label={i18n._(t`Timeout`)} tooltip={i18n._(t`The amount of time (in seconds) to run - before the task is canceled. Defaults to 0 for no job + before the job is canceled. Defaults to 0 for no job timeout.`)} /> Date: Tue, 5 Jan 2021 09:50:49 -0500 Subject: [PATCH 32/55] Adds sync button to project details page --- .../Project/ProjectDetail/ProjectDetail.jsx | 44 ++++++++++--------- .../ProjectDetail/ProjectDetail.test.jsx | 29 +++++++++++- .../Project/ProjectList/ProjectListItem.jsx | 19 ++------ .../Project/shared/ProjectSyncButton.jsx | 31 +++++++------ .../Project/shared/ProjectSyncButton.test.jsx | 8 +--- 5 files changed, 71 insertions(+), 60 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 380196d950..4c92c9695e 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -19,6 +19,7 @@ import CredentialChip from '../../../components/CredentialChip'; import { ProjectsAPI } from '../../../api'; import { toTitleCase } from '../../../util/strings'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import ProjectSyncButton from '../shared/ProjectSyncButton'; function ProjectDetail({ project, i18n }) { const { @@ -148,27 +149,28 @@ function ProjectDetail({ project, i18n }) { /> - {summary_fields.user_capabilities && - summary_fields.user_capabilities.edit && ( - - )} - {summary_fields.user_capabilities && - summary_fields.user_capabilities.delete && ( - - {i18n._(t`Delete`)} - - )} + {summary_fields.user_capabilities?.edit && ( + + )} + {summary_fields.user_capabilities?.start && ( + + )} + {summary_fields.user_capabilities?.delete && ( + + {i18n._(t`Delete`)} + + )} {/* Update delete modal to show dependencies https://github.com/ansible/awx/issues/5546 */} {error && ( diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx index 3139e2b14c..52e45e7d28 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -9,7 +9,12 @@ import { ProjectsAPI } from '../../../api'; import ProjectDetail from './ProjectDetail'; jest.mock('../../../api'); - +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useRouteMatch: () => ({ + url: '/projects/1/details', + }), +})); describe('', () => { const mockProject = { id: 1, @@ -139,13 +144,19 @@ describe('', () => { ); }); - test('should show edit button for users with edit permission', async () => { + test('should show edit and sync button for users with edit permission', async () => { const wrapper = mountWithContexts(); const editButton = await waitForElement( wrapper, 'ProjectDetail Button[aria-label="edit"]' ); + + const syncButton = await waitForElement( + wrapper, + 'ProjectDetail Button[aria-label="Sync Project"]' + ); expect(editButton.text()).toEqual('Edit'); + expect(syncButton.text()).toEqual('Sync'); expect(editButton.prop('to')).toBe(`/projects/${mockProject.id}/edit`); }); @@ -166,6 +177,9 @@ describe('', () => { expect(wrapper.find('ProjectDetail Button[aria-label="edit"]').length).toBe( 0 ); + expect(wrapper.find('ProjectDetail Button[aria-label="sync"]').length).toBe( + 0 + ); }); test('edit button should navigate to project edit', () => { @@ -180,6 +194,17 @@ describe('', () => { expect(history.location.pathname).toEqual('/projects/1/edit'); }); + test('sync button should call api to syn project', async () => { + ProjectsAPI.readSync.mockResolvedValue({ data: { can_update: true } }); + const wrapper = mountWithContexts(); + await act(() => + wrapper + .find('ProjectDetail Button[aria-label="Sync Project"]') + .prop('onClick')(1) + ); + expect(ProjectsAPI.sync).toHaveBeenCalledTimes(1); + }); + test('expected api calls are made for delete', async () => { const wrapper = mountWithContexts(); await waitForElement(wrapper, 'ProjectDetail Button[aria-label="Delete"]'); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index b2539e5f87..dba55552d4 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -14,7 +14,7 @@ import { import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { PencilAltIcon, SyncIcon } from '@patternfly/react-icons'; +import { PencilAltIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; import { formatDateString, timeOfDay } from '../../../util/dates'; import { ProjectsAPI } from '../../../api'; @@ -153,23 +153,10 @@ function ProjectListItem({ aria-labelledby={labelId} id={labelId} > - {project.summary_fields.user_capabilities.start ? ( + {project.summary_fields.user_capabilities.start && ( - - {handleSync => ( - - )} - + - ) : ( - '' )} {project.summary_fields.user_capabilities.edit ? ( diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx index b65aecae68..864142b046 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.jsx @@ -1,4 +1,8 @@ import React, { useCallback } from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import { Button } from '@patternfly/react-core'; +import { SyncIcon } from '@patternfly/react-icons'; + import { number } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -8,28 +12,27 @@ import AlertModal from '../../../components/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail'; import { ProjectsAPI } from '../../../api'; -function ProjectSyncButton({ i18n, children, projectId }) { +function ProjectSyncButton({ i18n, projectId }) { + const match = useRouteMatch(); + const { request: handleSync, error: syncError } = useRequest( useCallback(async () => { - const { data } = await ProjectsAPI.readSync(projectId); - if (data.can_update) { - await ProjectsAPI.sync(projectId); - } else { - throw new Error( - i18n._( - t`You don't have the necessary permissions to sync this project.` - ) - ); - } - }, [i18n, projectId]), + await ProjectsAPI.sync(projectId); + }, [projectId]), null ); const { error, dismissError } = useDismissableError(syncError); - + const isDetailsView = match.url.endsWith('/details'); return ( <> - {children(handleSync)} + {error && ( { let wrapper; - ProjectsAPI.readSync.mockResolvedValue({ - data: { - can_update: true, - }, - }); const children = handleSync => (