From b1ce5e24e3c5499440166a567766e6f86f64cdaf Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Mon, 22 Feb 2021 14:48:48 -0800 Subject: [PATCH 01/39] add expand button to VariablesField --- awx/ui_next/.eslintrc | 3 +- .../src/components/CodeEditor/CodeEditor.jsx | 14 ++- .../components/CodeEditor/VariablesDetail.jsx | 89 +++++++++----- .../components/CodeEditor/VariablesField.jsx | 113 +++++++++++++++--- .../src/components/DetailList/CodeDetail.jsx | 13 +- .../src/screens/Host/HostFacts/HostFacts.jsx | 2 +- .../InventoryHostFacts/InventoryHostFacts.jsx | 2 +- .../screens/Setting/shared/SharedFields.jsx | 2 +- 8 files changed, 172 insertions(+), 66 deletions(-) diff --git a/awx/ui_next/.eslintrc b/awx/ui_next/.eslintrc index f82900134e..b7c86c305a 100644 --- a/awx/ui_next/.eslintrc +++ b/awx/ui_next/.eslintrc @@ -77,7 +77,8 @@ "resizeOrientation", "src", "theme", - "gridColumns" + "gridColumns", + "rows" ], "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"], "ignoreComponent": [ diff --git a/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx index 0218a83c3c..981cf01610 100644 --- a/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx +++ b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx @@ -77,6 +77,13 @@ function CodeEditor({ className, i18n, }) { + if (typeof rows !== 'number' && rows !== 'auto') { + // eslint-disable-next-line no-console + console.warning( + `CodeEditor: Unexpected value for 'rows': ${rows}; expected number or 'auto'` + ); + } + const wrapper = useRef(null); const editor = useRef(null); @@ -117,7 +124,8 @@ function CodeEditor({ jinja2: 'django', }; - const numRows = fullHeight ? value.split('\n').length : rows; + const numRows = rows === 'auto' ? value.split('\n').length : rows; + const height = fullHeight ? '50vh' : `${numRows * LINE_HEIGHT + PADDING}px`; return ( <> @@ -132,7 +140,7 @@ function CodeEditor({ editorProps={{ $blockScrolling: true }} fontSize={16} width="100%" - height={`${numRows * LINE_HEIGHT + PADDING}px`} + height={height} hasErrors={hasErrors} setOptions={{ readOnly, @@ -178,7 +186,7 @@ CodeEditor.propTypes = { readOnly: bool, hasErrors: bool, fullHeight: bool, - rows: number, + rows: oneOf(number, string), className: string, }; CodeEditor.defaultProps = { diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx index f5cfd91373..602ef6695f 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx @@ -2,7 +2,14 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types'; import { Trans, withI18n } from '@lingui/react'; -import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core'; +import { t } from '@lingui/macro'; +import { + Split, + SplitItem, + TextListItemVariants, + Button, +} from '@patternfly/react-core'; +import { ExpandArrowsAltIcon } from '@patternfly/react-icons'; import { DetailName, DetailValue } from '../DetailList'; import MultiButtonToggle from '../MultiButtonToggle'; import Popover from '../Popover'; @@ -29,13 +36,22 @@ function getValueAsMode(value, mode) { return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value); } -function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) { +function VariablesDetail({ + dataCy, + helpText, + value, + label, + rows, + fullHeight, + i18n, +}) { const [mode, setMode] = useState( isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE ); const [currentValue, setCurrentValue] = useState( isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---' ); + const [isExpanded, setIsExpanded] = useState(false); const [error, setError] = useState(null); useEffect(() => { @@ -61,35 +77,48 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) { css="grid-column: 1 / -1" > - -
- - {label} - - {helpText && ( - - )} -
+ + + +
+ + {label} + + {helpText && ( + + )} +
+
+ + { + try { + setCurrentValue(getValueAsMode(currentValue, newMode)); + setMode(newMode); + } catch (err) { + setError(err); + } + }} + /> + +
- { - try { - setCurrentValue(getValueAsMode(currentValue, newMode)); - setMode(newMode); - } catch (err) { - setError(err); - } - }} - /> +
@@ -122,7 +151,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) { VariablesDetail.propTypes = { value: oneOfType([shape({}), arrayOf(string), string]).isRequired, label: node.isRequired, - rows: number, + rows: oneOfType(number, string), dataCy: string, helpText: string, }; diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx index 8fbf1e4bfa..35756132e8 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx @@ -4,7 +4,8 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { useField } from 'formik'; import styled from 'styled-components'; -import { Split, SplitItem } from '@patternfly/react-core'; +import { Split, SplitItem, Button, Modal } from '@patternfly/react-core'; +import { ExpandArrowsAltIcon } from '@patternfly/react-icons'; import { CheckboxField } from '../FormField'; import MultiButtonToggle from '../MultiButtonToggle'; import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; @@ -20,6 +21,7 @@ const FieldHeader = styled.div` const StyledCheckboxField = styled(CheckboxField)` --pf-c-check__label--FontSize: var(--pf-c-form__label--FontSize); + margin-left: auto; `; function VariablesField({ @@ -31,10 +33,91 @@ function VariablesField({ promptId, tooltip, }) { - const [field, meta, helpers] = useField(name); + const [field, meta] = useField(name); const [mode, setMode] = useState( isJsonString(field.value) ? JSON_MODE : YAML_MODE ); + const [isExpanded, setIsExpanded] = useState(false); + + return ( + <> + setIsExpanded(true)} + mode={mode} + setMode={setMode} + /> + setIsExpanded(false)} + actions={[ + , + ]} + > +
+ +
+
+ {meta.error ? ( +
+ {meta.error} +
+ ) : null} + + ); +} +VariablesField.propTypes = { + id: string.isRequired, + name: string.isRequired, + label: string.isRequired, + readOnly: bool, + promptId: string, +}; +VariablesField.defaultProps = { + readOnly: false, + promptId: null, +}; + +function VariablesFieldInternals({ + i18n, + id, + name, + label, + readOnly, + promptId, + tooltip, + fullHeight, + mode, + setMode, + onExpand, +}) { + const [field, meta, helpers] = useField(name); return (
@@ -75,6 +158,15 @@ function VariablesField({ name="ask_variables_on_launch" /> )} + {onExpand && ( + + )} { helpers.setValue(newVal); }} + fullHeight={fullHeight} hasErrors={!!meta.error} /> - {meta.error ? ( -
- {meta.error} -
- ) : null}
); } -VariablesField.propTypes = { - id: string.isRequired, - name: string.isRequired, - label: string.isRequired, - readOnly: bool, - promptId: string, -}; -VariablesField.defaultProps = { - readOnly: false, - promptId: null, -}; export default withI18n()(VariablesField); diff --git a/awx/ui_next/src/components/DetailList/CodeDetail.jsx b/awx/ui_next/src/components/DetailList/CodeDetail.jsx index 08c935b18e..90edb259e8 100644 --- a/awx/ui_next/src/components/DetailList/CodeDetail.jsx +++ b/awx/ui_next/src/components/DetailList/CodeDetail.jsx @@ -14,15 +14,7 @@ import { DetailName, DetailValue } from './Detail'; import CodeEditor from '../CodeEditor'; import Popover from '../Popover'; -function CodeDetail({ - value, - label, - mode, - rows, - fullHeight, - helpText, - dataCy, -}) { +function CodeDetail({ value, label, mode, rows, helpText, dataCy }) { const labelCy = dataCy ? `${dataCy}-label` : null; const valueCy = dataCy ? `${dataCy}-value` : null; @@ -57,7 +49,6 @@ function CodeDetail({ value={value} readOnly rows={rows} - fullHeight={fullHeight} css="margin-top: 10px" /> @@ -69,7 +60,7 @@ CodeDetail.propTypes = { label: node.isRequired, dataCy: string, helpText: string, - rows: number, + rows: oneOfType(number, string), mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired, }; CodeDetail.defaultProps = { diff --git a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx index f33c989f5b..08b447a991 100644 --- a/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx +++ b/awx/ui_next/src/screens/Host/HostFacts/HostFacts.jsx @@ -36,7 +36,7 @@ function HostFacts({ i18n, host }) { return ( - + ); diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx index 6bffd37ba4..4d93ce58c3 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostFacts/InventoryHostFacts.jsx @@ -35,7 +35,7 @@ function InventoryHostFacts({ i18n, host }) { return ( - + ); diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx index b5e118088a..96dc973de5 100644 --- a/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx +++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.jsx @@ -286,7 +286,7 @@ const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => { > { From d6a5a1e0d015d0156c07b8617a1f792177d15609 Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Tue, 23 Feb 2021 16:33:11 -0800 Subject: [PATCH 02/39] add expand button to VariablesDetail --- .../components/CodeEditor/VariablesDetail.jsx | 174 ++++++++++++------ 1 file changed, 117 insertions(+), 57 deletions(-) diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx index 602ef6695f..4c3af1fbe8 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx @@ -1,13 +1,14 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types'; -import { Trans, withI18n } from '@lingui/react'; +import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Split, SplitItem, TextListItemVariants, Button, + Modal, } from '@patternfly/react-core'; import { ExpandArrowsAltIcon } from '@patternfly/react-icons'; import { DetailName, DetailValue } from '../DetailList'; @@ -36,15 +37,7 @@ function getValueAsMode(value, mode) { return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value); } -function VariablesDetail({ - dataCy, - helpText, - value, - label, - rows, - fullHeight, - i18n, -}) { +function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { const [mode, setMode] = useState( isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE ); @@ -76,51 +69,18 @@ function VariablesDetail({ fullWidth css="grid-column: 1 / -1" > - - - - -
- - {label} - - {helpText && ( - - )} -
-
- - { - try { - setCurrentValue(getValueAsMode(currentValue, newMode)); - setMode(newMode); - } catch (err) { - setError(err); - } - }} - /> - -
-
- - - -
+ setIsExpanded(true)} + i18n={i18n} + /> {error && ( @@ -141,10 +100,48 @@ function VariablesDetail({ css="color: var(--pf-global--danger-color--100); font-size: var(--pf-global--FontSize--sm" > - Error: {error.message} + {i18n._(t`Error:`)} {error.message} )} + setIsExpanded(false)} + actions={[ + , + ]} + > +
+ + +
+
); } @@ -161,4 +158,67 @@ VariablesDetail.defaultProps = { helpText: '', }; +function ModeToggle({ + label, + helpText, + dataCy, + currentValue, + setCurrentValue, + mode, + setMode, + setError, + onExpand, + i18n, +}) { + return ( + + + + +
+ + {label} + + {helpText && ( + + )} +
+
+ + { + try { + setCurrentValue(getValueAsMode(currentValue, newMode)); + setMode(newMode); + } catch (err) { + setError(err); + } + }} + /> + +
+
+ {onExpand && ( + + + + )} +
+ ); +} + export default withI18n()(VariablesDetail); From f867c9e4761441e2880b154b0d082f8cee3ec44b Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Wed, 24 Feb 2021 08:54:36 -0800 Subject: [PATCH 03/39] fix type errors in VariablesDetail --- awx/ui_next/src/components/CodeEditor/CodeEditor.jsx | 6 +++--- awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx | 2 +- .../src/components/CodeEditor/VariablesField.test.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx index 981cf01610..ad930b7a20 100644 --- a/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx +++ b/awx/ui_next/src/components/CodeEditor/CodeEditor.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useCallback } from 'react'; -import { oneOf, bool, number, string, func } from 'prop-types'; +import { oneOf, bool, number, string, func, oneOfType } from 'prop-types'; import ReactAce from 'react-ace'; import 'ace-builds/src-noconflict/mode-json'; import 'ace-builds/src-noconflict/mode-javascript'; @@ -77,7 +77,7 @@ function CodeEditor({ className, i18n, }) { - if (typeof rows !== 'number' && rows !== 'auto') { + if (rows && typeof rows !== 'number' && rows !== 'auto') { // eslint-disable-next-line no-console console.warning( `CodeEditor: Unexpected value for 'rows': ${rows}; expected number or 'auto'` @@ -186,7 +186,7 @@ CodeEditor.propTypes = { readOnly: bool, hasErrors: bool, fullHeight: bool, - rows: oneOf(number, string), + rows: oneOfType([number, string]), className: string, }; CodeEditor.defaultProps = { diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx index 4c3af1fbe8..fa46a60f40 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx @@ -148,7 +148,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { VariablesDetail.propTypes = { value: oneOfType([shape({}), arrayOf(string), string]).isRequired, label: node.isRequired, - rows: oneOfType(number, string), + rows: oneOfType([number, string]), dataCy: string, helpText: string, }; diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx index e07ff9d40b..37cdc8a53d 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx @@ -32,7 +32,7 @@ describe('VariablesField', () => { ); const buttons = wrapper.find('Button'); - expect(buttons).toHaveLength(2); + expect(buttons).toHaveLength(3); expect(buttons.at(0).prop('variant')).toEqual('primary'); expect(buttons.at(1).prop('variant')).toEqual('secondary'); await act(async () => { From 659f68f280ad1b288acabc0dae7238cc76e9945b Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Thu, 25 Feb 2021 09:44:45 -0800 Subject: [PATCH 04/39] add VariablesField expand test --- .../CodeEditor/VariablesField.test.jsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx index 37cdc8a53d..24c896069e 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.test.jsx @@ -136,4 +136,27 @@ describe('VariablesField', () => { expect(wrapper.find('CodeEditor').prop('mode')).toEqual('javascript'); }); + + it('should open modal when expanded', async () => { + const value = '---'; + const wrapper = mountWithContexts( + + {formik => ( +
+ + + + )} +
+ ); + expect(wrapper.find('Modal').prop('isOpen')).toEqual(false); + + wrapper.find('Button[variant="plain"]').invoke('onClick')(); + wrapper.update(); + + expect(wrapper.find('Modal').prop('isOpen')).toEqual(true); + expect(wrapper.find('Modal CodeEditor')).toHaveLength(1); + }); }); From e8886a552563a2195af77331c712be0295cd2ae4 Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Mon, 15 Mar 2021 11:42:09 -0700 Subject: [PATCH 05/39] fix InventoryGroupDetails test --- .../InventoryGroupDetail/InventoryGroupDetail.test.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx index a4c584c121..dc7e47b646 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx @@ -72,11 +72,12 @@ describe('', () => { }); test('should open delete modal and then call api to delete the group', async () => { + expect(wrapper.find('Modal').length).toBe(1); // variables modal already mounted await act(async () => { wrapper.find('button[aria-label="Delete"]').simulate('click'); }); - await waitForElement(wrapper, 'Modal', el => el.length === 1); - expect(wrapper.find('Modal').length).toBe(1); + wrapper.update(); + expect(wrapper.find('Modal').length).toBe(2); await act(async () => { wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); }); From fb257d0add5e2c55cc900e3d43aa0ae4f9433a8f Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Wed, 24 Mar 2021 11:45:19 -0700 Subject: [PATCH 06/39] add ouia ids to variables expand buttons --- awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx | 2 ++ awx/ui_next/src/components/CodeEditor/VariablesField.jsx | 2 ++ .../screens/Template/JobTemplateDetail/JobTemplateDetail.jsx | 1 + 3 files changed, 5 insertions(+) diff --git a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx index fa46a60f40..797c10673d 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesDetail.jsx @@ -115,6 +115,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, i18n }) { key="select" variant="primary" onClick={() => setIsExpanded(false)} + ouiaId={`${dataCy}-unexpand`} > {i18n._(t`Done`)} , @@ -212,6 +213,7 @@ function ModeToggle({ variant="plain" aria-label={i18n._(t`Expand input`)} onClick={onExpand} + ouiaId={`${dataCy}-expand`} > diff --git a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx index 35756132e8..6f039c6895 100644 --- a/awx/ui_next/src/components/CodeEditor/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeEditor/VariablesField.jsx @@ -64,6 +64,7 @@ function VariablesField({ key="select" variant="primary" onClick={() => setIsExpanded(false)} + ouiaId={`${id}-variables-unexpand`} > {i18n._(t`Done`)} , @@ -163,6 +164,7 @@ function VariablesFieldInternals({ variant="plain" aria-label={i18n._(t`Expand input`)} onClick={onExpand} + ouiaId={`${id}-variables-expand`} > diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index d61819adcc..1a5cf4a4b1 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -372,6 +372,7 @@ function JobTemplateDetail({ i18n, template }) { value={extra_vars} rows={4} label={i18n._(t`Variables`)} + dataCy={`jt-details-${template.id}`} /> From cdb2832f05a769751b1a81939512749a4cdd2301 Mon Sep 17 00:00:00 2001 From: beeankha Date: Thu, 25 Mar 2021 13:50:02 -0400 Subject: [PATCH 07/39] Bump up version of iPython to be compatible with Python 3.8.5 --- requirements/requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index df8cc1cb13..a1bae58598 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,7 +1,7 @@ django-debug-toolbar==1.11 django-rest-swagger pprofile -ipython==5.2.1 +ipython==7.21.0 unittest2 black pytest From d3eb66b6fe52d7da1855c6a4d885ac344f9cf580 Mon Sep 17 00:00:00 2001 From: nixocio Date: Wed, 24 Mar 2021 12:45:16 -0400 Subject: [PATCH 08/39] Update RBAC for EE Update RBAC for EE details page. See: https://github.com/ansible/awx/issues/9416 --- .../ExecutionEnvironmentDetails.jsx | 46 ++++++----- .../ExecutionEnvironmentDetails.test.jsx | 78 ++++++++++++++++++- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index a5c89f8d89..35e4209fc8 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -110,27 +110,31 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { {!managedByTower && ( - - - {i18n._(t`Delete`)} - + {summary_fields.user_capabilities?.edit && ( + + )} + {summary_fields.user_capabilities?.delete && ( + + {i18n._(t`Delete`)} + + )} )} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx index ce0bf830ed..19dd4e1e16 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.test.jsx @@ -2,7 +2,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails'; @@ -22,6 +25,11 @@ const executionEnvironment = { credential: '/api/v2/credentials/4/', }, summary_fields: { + user_capabilities: { + edit: true, + delete: true, + copy: true, + }, credential: { id: 4, name: 'Container Registry', @@ -175,6 +183,7 @@ describe('', () => { expect(wrapper.find('Button[aria-label="Delete"]')).toHaveLength(0); }); + test('should have proper number of delete detail requests', async () => { const history = createMemoryHistory({ initialEntries: ['/execution_environments/42/details'], @@ -193,4 +202,71 @@ describe('', () => { wrapper.find('DeleteButton').prop('deleteDetailsRequests') ).toHaveLength(4); }); + + test('should show edit button for users with edit permission', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + const editButton = await waitForElement( + wrapper, + 'ExecutionEnvironmentDetails Button[aria-label="edit"]' + ); + expect(editButton.text()).toEqual('Edit'); + expect(editButton.prop('to')).toBe('/execution_environments/17/edit'); + }); + + test('should hide edit button for users without edit permission', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ExecutionEnvironmentDetails'); + expect( + wrapper.find('ExecutionEnvironmentDetails Button[aria-label="edit"]') + .length + ).toBe(0); + }); + + test('should show delete button for users with delete permission', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + const deleteButton = await waitForElement( + wrapper, + 'ExecutionEnvironmentDetails Button[aria-label="Delete"]' + ); + expect(deleteButton.text()).toEqual('Delete'); + }); + + test('should hide delete button for users without delete permission', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ExecutionEnvironmentDetails'); + expect( + wrapper.find('ExecutionEnvironmentDetails Button[aria-label="Delete"]') + .length + ).toBe(0); + }); }); From dbcdbe0770641d2e6e0083d42a020b045f6520d8 Mon Sep 17 00:00:00 2001 From: sean-m-ssullivan Date: Sun, 28 Mar 2021 22:35:38 -0500 Subject: [PATCH 09/39] add logic to changed status --- awx_collection/plugins/modules/tower_project_update.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_project_update.py b/awx_collection/plugins/modules/tower_project_update.py index 0ee764a5e4..42be06ea12 100644 --- a/awx_collection/plugins/modules/tower_project_update.py +++ b/awx_collection/plugins/modules/tower_project_update.py @@ -34,6 +34,7 @@ options: wait: description: - Wait for the project to update. + - If scm revision has not changed module will return not changed. default: True type: bool interval: @@ -109,6 +110,9 @@ def main(): if project is None: module.fail_json(msg="Unable to find project") + if wait: + scm_revision_original = project['scm_revision'] + # Update the project result = module.post_endpoint(project['related']['update']) @@ -126,7 +130,10 @@ def main(): start = time.time() # Invoke wait function - module.wait_on_url(url=result['json']['url'], object_name=module.get_item_name(project), object_type='Project Update', timeout=timeout, interval=interval) + result = module.wait_on_url(url=result['json']['url'], object_name=module.get_item_name(project), object_type='Project Update', timeout=timeout, interval=interval) + scm_revision_new = result['json']['scm_revision'] + if scm_revision_new == scm_revision_original: + module.json_output['changed'] = False module.exit_json(**module.json_output) From c2b5ffcc1c2c2bc69e8dcb2abfee6020b9e26d39 Mon Sep 17 00:00:00 2001 From: sean-m-ssullivan Date: Mon, 29 Mar 2021 00:21:29 -0500 Subject: [PATCH 10/39] linting --- awx_collection/plugins/modules/tower_project_update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_project_update.py b/awx_collection/plugins/modules/tower_project_update.py index 42be06ea12..796f910df7 100644 --- a/awx_collection/plugins/modules/tower_project_update.py +++ b/awx_collection/plugins/modules/tower_project_update.py @@ -130,7 +130,9 @@ def main(): start = time.time() # Invoke wait function - result = module.wait_on_url(url=result['json']['url'], object_name=module.get_item_name(project), object_type='Project Update', timeout=timeout, interval=interval) + result = module.wait_on_url( + url=result['json']['url'], object_name=module.get_item_name(project), object_type='Project Update', timeout=timeout, interval=interval + ) scm_revision_new = result['json']['scm_revision'] if scm_revision_new == scm_revision_original: module.json_output['changed'] = False From 8fb393c0a143bb078f5e6072916cabdaf7d9779c Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 09:11:56 -0400 Subject: [PATCH 11/39] Fix awxkit function that detects private data directories in job args --- awxkit/awxkit/api/pages/unified_jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awxkit/awxkit/api/pages/unified_jobs.py b/awxkit/awxkit/api/pages/unified_jobs.py index 09dea1ebbb..4f71c2eb6c 100644 --- a/awxkit/awxkit/api/pages/unified_jobs.py +++ b/awxkit/awxkit/api/pages/unified_jobs.py @@ -139,7 +139,7 @@ class UnifiedJob(HasStatus, base.Base): """ self.get() job_args = self.job_args - expected_prefix = '/tmp/awx_{}'.format(self.id) + expected_prefix = '/tmp/pdd_wrapper_{}'.format(self.id) for arg1, arg2 in zip(job_args[:-1], job_args[1:]): if arg1 == '-v': if ':' in arg2: From b6ccd02f3d7e83a3d1d7e8015dea211c9369016a Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 29 Mar 2021 10:39:22 -0400 Subject: [PATCH 12/39] Update the versioning on the docker-compose template Some versions of docker-compose will break with the new addition of name parameters without this. --- .../ansible/roles/sources/templates/docker-compose.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index 3b32df378e..34d32df891 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -1,5 +1,5 @@ --- -version: '2' +version: '2.1' services: {% for i in range(cluster_node_count|int) %} {% set container_postfix = loop.index %} From 675286c1ac3f039ccff8e92a5be077ca4e39a8ff Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 29 Mar 2021 11:52:20 -0400 Subject: [PATCH 13/39] Enable ?page_size=1 in URL to fetch correct objects on schedules endpoint --- .../0135_schedule_sort_fallback_to_id.py | 18 ++++++++++++++++++ awx/main/models/schedules.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0135_schedule_sort_fallback_to_id.py diff --git a/awx/main/migrations/0135_schedule_sort_fallback_to_id.py b/awx/main/migrations/0135_schedule_sort_fallback_to_id.py new file mode 100644 index 0000000000..69969fafb4 --- /dev/null +++ b/awx/main/migrations/0135_schedule_sort_fallback_to_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2021-03-29 15:30 + +from django.db import migrations +import django.db.models.expressions + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0134_unifiedjob_ansible_version'), + ] + + operations = [ + migrations.AlterModelOptions( + name='schedule', + options={'ordering': [django.db.models.expressions.OrderBy(django.db.models.expressions.F('next_run'), descending=True, nulls_last=True), 'id']}, + ), + ] diff --git a/awx/main/models/schedules.py b/awx/main/models/schedules.py index d30d44372d..dca50d9232 100644 --- a/awx/main/models/schedules.py +++ b/awx/main/models/schedules.py @@ -63,7 +63,7 @@ class ScheduleManager(ScheduleFilterMethods, models.Manager): class Schedule(PrimordialModel, LaunchTimeConfig): class Meta: app_label = 'main' - ordering = ['-next_run'] + ordering = [models.F('next_run').desc(nulls_last=True), 'id'] unique_together = ('unified_job_template', 'name') objects = ScheduleManager() From 115a344842f9f6811ad096ec97c11f5cf6922317 Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 29 Mar 2021 10:35:22 -0400 Subject: [PATCH 14/39] Add templates screen to EE Add templates screen to EE. See: https://github.com/ansible/awx/issues/9723 --- .../src/api/models/ExecutionEnvironments.js | 10 ++ awx/ui_next/src/api/models/Organizations.js | 6 +- .../ExecutionEnvironment.jsx | 11 ++ .../ExecutionEnvironmentTemplateList.jsx | 139 ++++++++++++++++++ .../ExecutionEnvironmentTemplateList.test.jsx | 116 +++++++++++++++ .../ExecutionEnvironmentTemplateListItem.jsx | 43 ++++++ ...cutionEnvironmentTemplateListItem.test.jsx | 48 ++++++ .../ExecutionEnvironmentTemplate/index.js | 1 + .../OrganizationExecEnvList.jsx | 2 +- 9 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx create mode 100644 awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js diff --git a/awx/ui_next/src/api/models/ExecutionEnvironments.js b/awx/ui_next/src/api/models/ExecutionEnvironments.js index 2df933d53a..ae3d128ed3 100644 --- a/awx/ui_next/src/api/models/ExecutionEnvironments.js +++ b/awx/ui_next/src/api/models/ExecutionEnvironments.js @@ -5,6 +5,16 @@ class ExecutionEnvironments extends Base { super(http); this.baseUrl = '/api/v2/execution_environments/'; } + + readUnifiedJobTemplates(id, params) { + return this.http.get(`${this.baseUrl}${id}/unified_job_templates/`, { + params, + }); + } + + readUnifiedJobTemplateOptions(id) { + return this.http.options(`${this.baseUrl}${id}/unified_job_templates/`); + } } export default ExecutionEnvironments; diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index fd980fece8..a2baa4f9c8 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -36,10 +36,8 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { }); } - readExecutionEnvironmentsOptions(id, params) { - return this.http.options(`${this.baseUrl}${id}/execution_environments/`, { - params, - }); + readExecutionEnvironmentsOptions(id) { + return this.http.options(`${this.baseUrl}${id}/execution_environments/`); } createUser(id, data) { diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx index 55a3228e13..bb86dc2f57 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironment.jsx @@ -20,6 +20,7 @@ import ContentLoading from '../../components/ContentLoading'; import ExecutionEnvironmentDetails from './ExecutionEnvironmentDetails'; import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; +import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplate'; function ExecutionEnvironment({ i18n, setBreadcrumb }) { const { id } = useParams(); @@ -64,6 +65,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) { link: `/execution_environments/${id}/details`, id: 0, }, + { + name: i18n._(t`Templates`), + link: `/execution_environments/${id}/templates`, + id: 1, + }, ]; if (!isLoading && contentError) { @@ -114,6 +120,11 @@ function ExecutionEnvironment({ i18n, setBreadcrumb }) { executionEnvironment={executionEnvironment} /> + + + )} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx new file mode 100644 index 0000000000..c2b36eee30 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.jsx @@ -0,0 +1,139 @@ +import React, { useEffect, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card } from '@patternfly/react-core'; + +import { ExecutionEnvironmentsAPI } from '../../../api'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import useRequest from '../../../util/useRequest'; +import DatalistToolbar from '../../../components/DataListToolbar'; +import PaginatedDataList from '../../../components/PaginatedDataList'; + +import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem'; + +const QS_CONFIG = getQSConfig( + 'execution_environments', + { + page: 1, + page_size: 20, + order_by: 'name', + type: 'job_template,workflow_job_template', + }, + ['id', 'page', 'page_size'] +); + +function ExecutionEnvironmentTemplateList({ i18n, executionEnvironment }) { + const { id } = executionEnvironment; + const location = useLocation(); + + const { + error: contentError, + isLoading, + request: fetchTemplates, + result: { + templates, + templatesCount, + relatedSearchableKeys, + searchableKeys, + }, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + + const [response, responseActions] = await Promise.all([ + ExecutionEnvironmentsAPI.readUnifiedJobTemplates(id, params), + ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions(id), + ]); + + return { + templates: response.data.results, + templatesCount: response.data.count, + actions: responseActions.data.actions, + relatedSearchableKeys: ( + responseActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + responseActions.data.actions?.GET || {} + ).filter(key => responseActions.data.actions?.GET[key].filterable), + }; + }, [location, id]), + { + templates: [], + templatesCount: 0, + actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + useEffect(() => { + fetchTemplates(); + }, [fetchTemplates]); + + return ( + <> + + ( + + )} + renderItem={template => ( + + )} + /> + + + ); +} + +export default withI18n()(ExecutionEnvironmentTemplateList); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx new file mode 100644 index 0000000000..078d6d249d --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateList.test.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; + +import { ExecutionEnvironmentsAPI } from '../../../api'; +import ExecutionEnvironmentTemplateList from './ExecutionEnvironmentTemplateList'; + +jest.mock('../../../api/'); + +const templates = { + data: { + count: 3, + results: [ + { + id: 1, + type: 'job_template', + name: 'Foo', + url: '/api/v2/job_templates/1/', + related: { + execution_environment: '/api/v2/execution_environments/1/', + }, + }, + { + id: 2, + type: 'workflow_job_template', + name: 'Bar', + url: '/api/v2/workflow_job_templates/2/', + related: { + execution_environment: '/api/v2/execution_environments/1/', + }, + }, + { + id: 3, + type: 'job_template', + name: 'Fuzz', + url: '/api/v2/job_templates/3/', + related: { + execution_environment: '/api/v2/execution_environments/1/', + }, + }, + ], + }, +}; + +const mockExecutionEnvironment = { + id: 1, + name: 'Default EE', +}; + +const options = { data: { actions: { GET: {} } } }; + +describe('', () => { + let wrapper; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentTemplateList', + el => el.length > 0 + ); + }); + + test('should have data fetched and render 3 rows', async () => { + ExecutionEnvironmentsAPI.readUnifiedJobTemplates.mockResolvedValue( + templates + ); + + ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions.mockResolvedValue( + options + ); + + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement( + wrapper, + 'ExecutionEnvironmentTemplateList', + el => el.length > 0 + ); + + expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(3); + expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplates).toBeCalled(); + expect(ExecutionEnvironmentsAPI.readUnifiedJobTemplateOptions).toBeCalled(); + }); + + test('should not render add button', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + waitForElement( + wrapper, + 'ExecutionEnvironmentTemplateList', + el => el.length > 0 + ); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx new file mode 100644 index 0000000000..4a33126386 --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import { + DataListItem, + DataListItemRow, + DataListItemCells, +} from '@patternfly/react-core'; + +import DataListCell from '../../../components/DataListCell'; + +function ExecutionEnvironmentTemplateListItem({ template, detailUrl, i18n }) { + return ( + + + + + {template.name} + + , + + {template.type === 'job_template' + ? i18n._(t`Job Template`) + : i18n._(t`Workflow Job Template`)} + , + ]} + /> + + + ); +} + +export default withI18n()(ExecutionEnvironmentTemplateListItem); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx new file mode 100644 index 0000000000..9c107ab19b --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/ExecutionEnvironmentTemplateListItem.test.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import ExecutionEnvironmentTemplateListItem from './ExecutionEnvironmentTemplateListItem'; + +describe('', () => { + let wrapper; + const template = { + id: 1, + name: 'Foo', + type: 'job_template', + }; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1); + expect(wrapper.find('DataListCell[aria-label="Name"]').text()).toBe( + template.name + ); + expect( + wrapper.find('DataListCell[aria-label="Template type"]').text() + ).toBe('Job Template'); + }); + + test('should distinguish template types', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('ExecutionEnvironmentTemplateListItem').length).toBe(1); + expect( + wrapper.find('DataListCell[aria-label="Template type"]').text() + ).toBe('Workflow Job Template'); + }); +}); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js new file mode 100644 index 0000000000..3bdea254ee --- /dev/null +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentTemplate/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentTemplateList'; diff --git a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx index 9f2c4ae817..d567c24097 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationExecEnvList/OrganizationExecEnvList.jsx @@ -38,7 +38,7 @@ function OrganizationExecEnvList({ i18n, organization }) { const [response, responseActions] = await Promise.all([ OrganizationsAPI.readExecutionEnvironments(id, params), - OrganizationsAPI.readExecutionEnvironmentsOptions(id, params), + OrganizationsAPI.readExecutionEnvironmentsOptions(id), ]); return { From 1e9b22148668a0168ab5cb212a503beeea05d5fe Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:05:57 -0400 Subject: [PATCH 15/39] Use revolved EE for Container Group tasks --- awx/main/tasks.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b5cb7ec1e8..6cadc1973e 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3126,11 +3126,20 @@ class AWXReceptorJob: @property def pod_definition(self): + ee = self.task.instance.resolve_execution_environment() + default_pod_spec = { "apiVersion": "v1", "kind": "Pod", "metadata": {"namespace": settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE}, - "spec": {"containers": [{"image": settings.AWX_CONTAINER_GROUP_DEFAULT_IMAGE, "name": 'worker', "args": ['ansible-runner', 'worker']}]}, + "spec": { + "containers": [ + { + "image": ee.image, + "name": 'worker', + } + ], + }, } pod_spec_override = {} From eeb6aaaea955675304892001a88ecb2ca19c5893 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:08:43 -0400 Subject: [PATCH 16/39] Always use EE resolving logic --- awx/main/management/commands/create_preload_data.py | 6 +++--- awx/main/management/commands/inventory_import.py | 3 ++- awx/main/models/mixins.py | 10 ++-------- awx/main/utils/execution_environments.py | 9 +++++++++ awx/settings/defaults.py | 13 +++++++------ 5 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 awx/main/utils/execution_environments.py diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index af5d8d9d9b..b40515321d 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -68,12 +68,12 @@ class Command(BaseCommand): print('Demo Credential, Inventory, and Job Template added.') changed = True - default_ee = settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE - ee, created = ExecutionEnvironment.objects.get_or_create(name='Default EE', defaults={'image': default_ee, 'managed_by_tower': True}) + for ee in reversed(settings.DEFAULT_EXECUTION_ENVIRONMENTS): + _, created = ExecutionEnvironment.objects.get_or_create(name=ee['name'], defaults={'image': ee['image'], 'managed_by_tower': True}) if created: changed = True - print('Default Execution Environment registered.') + print('Default Execution Environment(s) registered.') if changed: print('(changed: True)') diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 9cdd2f3017..af359128eb 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -29,6 +29,7 @@ from awx.main.utils.safe_yaml import sanitize_jinja # other AWX imports from awx.main.models.rbac import batch_role_ancestor_rebuilding from awx.main.utils import ignore_inventory_computed_fields, get_licenser +from awx.main.utils.execution_environments import get_execution_environment_default from awx.main.signals import disable_activity_stream from awx.main.constants import STANDARD_INVENTORY_UPDATE_ENV from awx.main.utils.pglock import advisory_lock @@ -90,7 +91,7 @@ class AnsibleInventoryLoader(object): bargs.extend(['-v', '{0}:{0}:Z'.format(self.source)]) for key, value in STANDARD_INVENTORY_UPDATE_ENV.items(): bargs.extend(['-e', '{0}={1}'.format(key, value)]) - bargs.extend([settings.AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE]) + bargs.extend([get_execution_environment_default().image]) bargs.extend(['ansible-inventory', '-i', self.source]) bargs.extend(['--playbook-dir', functioning_dir(self.source)]) if self.verbosity: diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 8055502096..645d0ebe09 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -21,6 +21,7 @@ from django.utils.translation import ugettext_lazy as _ from awx.main.models.base import prevent_search from awx.main.models.rbac import Role, RoleAncestorEntry, get_roles_on_resource from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic +from awx.main.utils.execution_environments import get_execution_environment_default from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted from awx.main.utils.polymorphic import build_polymorphic_ctypes_map from awx.main.fields import JSONField, AskForField @@ -461,13 +462,6 @@ class ExecutionEnvironmentMixin(models.Model): help_text=_('The container image to be used for execution.'), ) - def get_execution_environment_default(self): - from awx.main.models.execution_environments import ExecutionEnvironment - - if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: - return settings.DEFAULT_EXECUTION_ENVIRONMENT - return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() - def resolve_execution_environment(self): """ Return the execution environment that should be used when creating a new job. @@ -482,7 +476,7 @@ class ExecutionEnvironmentMixin(models.Model): if self.inventory.organization.default_environment is not None: return self.inventory.organization.default_environment - return self.get_execution_environment_default() + return get_execution_environment_default() class CustomVirtualEnvMixin(models.Model): diff --git a/awx/main/utils/execution_environments.py b/awx/main/utils/execution_environments.py new file mode 100644 index 0000000000..d705f93210 --- /dev/null +++ b/awx/main/utils/execution_environments.py @@ -0,0 +1,9 @@ +from django.conf import settings + +from awx.main.models.execution_environments import ExecutionEnvironment + + +def get_execution_environment_default(): + if settings.DEFAULT_EXECUTION_ENVIRONMENT is not None: + return settings.DEFAULT_EXECUTION_ENVIRONMENT + return ExecutionEnvironment.objects.filter(organization=None, managed_by_tower=True).first() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 2daa33d4b3..11208e82e0 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -68,17 +68,11 @@ DATABASES = { # the K8S cluster where awx itself is running) IS_K8S = False -# TODO: remove this setting in favor of a default execution environment -AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE = 'quay.io/ansible/awx-ee' - AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = os.getenv('MY_POD_NAMESPACE', 'default') -# TODO: remove this setting in favor of a default execution environment -AWX_CONTAINER_GROUP_DEFAULT_IMAGE = AWX_EXECUTION_ENVIRONMENT_DEFAULT_IMAGE - # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ # @@ -182,8 +176,15 @@ REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] PROXY_IP_ALLOWED_LIST = [] CUSTOM_VENV_PATHS = [] + +# Warning: this is a placeholder for a configure tower-in-tower setting +# This should not be set via a file. DEFAULT_EXECUTION_ENVIRONMENT = None +# This list is used for creating default EEs when running awx-manage create_preload_data. +# Should be ordered from highest to lowest precedence. +DEFAULT_EXECUTION_ENVIRONMENTS = [{'name': 'AWX EE 0.1.1', 'image': 'quay.io/ansible/awx-ee:0.1.1'}] + # Note: This setting may be overridden by database settings. STDOUT_MAX_BYTES_DISPLAY = 1048576 From 9d17d40b86810b69ae6dd058db6f1e6bd24796df Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:09:31 -0400 Subject: [PATCH 17/39] Add setting for keeping container group pod after job run Helpful for debugging --- awx/main/tasks.py | 2 +- awx/settings/defaults.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 6cadc1973e..43a629b288 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -3010,7 +3010,7 @@ class AWXReceptorJob: return self._run_internal(receptor_ctl) finally: # Make sure to always release the work unit if we established it - if self.unit_id is not None: + if self.unit_id is not None and not settings.AWX_CONTAINER_GROUP_KEEP_POD: receptor_ctl.simple_command(f"work release {self.unit_id}") def _run_internal(self, receptor_ctl): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 11208e82e0..28f5b20ec8 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -68,6 +68,7 @@ DATABASES = { # the K8S cluster where awx itself is running) IS_K8S = False +AWX_CONTAINER_GROUP_KEEP_POD = True AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 From 9bc0bf0ee728d95a715f2a6db9ccd5c68961010e Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:09:56 -0400 Subject: [PATCH 18/39] Fix inventory updates when running inside of k8s --- awx/main/models/inventory.py | 4 ++++ awx/main/tasks.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index cbec2963ca..6fabdf7567 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1227,6 +1227,10 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin, null=True, ) + @property + def is_container_group_task(self): + return bool(self.instance_group and self.instance_group.is_container_group) + def _get_parent_field_name(self): return 'inventory_source' diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 43a629b288..e05045574f 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2505,7 +2505,7 @@ class RunInventoryUpdate(BaseTask): args.append(container_location) args.append('--output') - args.append(os.path.join('/runner', 'artifacts', 'output.json')) + args.append(os.path.join('/runner', 'artifacts', str(inventory_update.id), 'output.json')) if os.path.isdir(source_location): playbook_dir = container_location From 91351f7e3bfaf6343db543b470fa8366b23c53e0 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:13:05 -0400 Subject: [PATCH 19/39] Fix default setting for KEEP_POD --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 28f5b20ec8..b6a3966647 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -68,7 +68,7 @@ DATABASES = { # the K8S cluster where awx itself is running) IS_K8S = False -AWX_CONTAINER_GROUP_KEEP_POD = True +AWX_CONTAINER_GROUP_KEEP_POD = False AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRIES = 100 AWX_CONTAINER_GROUP_POD_LAUNCH_RETRY_DELAY = 5 From 3d533e566145d0f2774e7297737564236cae01c7 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 13:26:59 -0400 Subject: [PATCH 20/39] Fix container group OPTIONS request --- awx/main/tasks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index e05045574f..221a9ce600 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -97,6 +97,7 @@ from awx.main.utils import ( deepmerge, parse_yaml_or_json, ) +from awx.main.utils.execution_environments import get_execution_environment_default from awx.main.utils.ansible import read_ansible_config from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja @@ -3126,7 +3127,10 @@ class AWXReceptorJob: @property def pod_definition(self): - ee = self.task.instance.resolve_execution_environment() + if self.task: + ee = self.task.instance.resolve_execution_environment() + else: + ee = get_execution_environment_default() default_pod_spec = { "apiVersion": "v1", From f80c2cbfc3af84fe8f23534476e9d3ffc10d09a0 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 14:45:03 -0400 Subject: [PATCH 21/39] Delete test for unused code --- .../tests/unit/scheduler/test_kubernetes.py | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 awx/main/tests/unit/scheduler/test_kubernetes.py diff --git a/awx/main/tests/unit/scheduler/test_kubernetes.py b/awx/main/tests/unit/scheduler/test_kubernetes.py deleted file mode 100644 index 1f51401fe4..0000000000 --- a/awx/main/tests/unit/scheduler/test_kubernetes.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -from django.conf import settings - -from awx.main.models import ( - InstanceGroup, - Job, - JobTemplate, - Project, - Inventory, -) -from awx.main.scheduler.kubernetes import PodManager - - -@pytest.fixture -def container_group(): - instance_group = InstanceGroup(name='container-group', id=1) - - return instance_group - - -@pytest.fixture -def job(container_group): - return Job(pk=1, id=1, project=Project(), instance_group=container_group, inventory=Inventory(), job_template=JobTemplate(id=1, name='foo')) - - -def test_default_pod_spec(job): - default_image = PodManager(job).pod_definition['spec']['containers'][0]['image'] - assert default_image == settings.AWX_CONTAINER_GROUP_DEFAULT_IMAGE - - -def test_custom_pod_spec(job): - job.instance_group.pod_spec_override = """ - spec: - containers: - - image: my-custom-image - """ - custom_image = PodManager(job).pod_definition['spec']['containers'][0]['image'] - assert custom_image == 'my-custom-image' - - -def test_pod_manager_namespace_property(job): - pm = PodManager(job) - assert pm.namespace == settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE - - job.instance_group.pod_spec_override = """ - metadata: - namespace: my-namespace - """ - assert PodManager(job).namespace == 'my-namespace' From a5b29201a4bb5f6141446298b969b6b60d103051 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 14:45:21 -0400 Subject: [PATCH 22/39] Update tests to use ee fixture --- awx/main/tests/functional/api/test_instance_group.py | 2 +- awx/main/tests/functional/conftest.py | 4 ++-- .../task_management/test_container_groups.py | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/awx/main/tests/functional/api/test_instance_group.py b/awx/main/tests/functional/api/test_instance_group.py index 967775dd74..c3cf44fd74 100644 --- a/awx/main/tests/functional/api/test_instance_group.py +++ b/awx/main/tests/functional/api/test_instance_group.py @@ -140,7 +140,7 @@ def test_delete_instance_group_jobs_running(delete, instance_group_jobs_running, @pytest.mark.django_db -def test_delete_rename_tower_instance_group_prevented(delete, options, tower_instance_group, instance_group, user, patch): +def test_delete_rename_tower_instance_group_prevented(delete, options, tower_instance_group, instance_group, user, patch, execution_environment): url = reverse("api:instance_group_detail", kwargs={'pk': tower_instance_group.pk}) super_user = user('bob', True) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 96101ffb41..c54b06b86f 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -829,5 +829,5 @@ def slice_job_factory(slice_jt_factory): @pytest.fixture -def execution_environment(organization): - return ExecutionEnvironment.objects.create(name="test-ee", description="test-ee", organization=organization) +def execution_environment(): + return ExecutionEnvironment.objects.create(name="test-ee", description="test-ee", managed_by_tower=True) diff --git a/awx/main/tests/functional/task_management/test_container_groups.py b/awx/main/tests/functional/task_management/test_container_groups.py index e88ef2deb1..7bbdac218d 100644 --- a/awx/main/tests/functional/task_management/test_container_groups.py +++ b/awx/main/tests/functional/task_management/test_container_groups.py @@ -1,10 +1,11 @@ import subprocess import base64 +from collections import namedtuple from unittest import mock # noqa import pytest -from awx.main.scheduler.kubernetes import PodManager +from awx.main.tasks import AWXReceptorJob from awx.main.utils import ( create_temporary_fifo, ) @@ -34,7 +35,7 @@ def test_containerized_job(containerized_job): @pytest.mark.django_db -def test_kubectl_ssl_verification(containerized_job): +def test_kubectl_ssl_verification(containerized_job, execution_environment): cred = containerized_job.instance_group.credential cred.inputs['verify_ssl'] = True key_material = subprocess.run('openssl genrsa 2> /dev/null', shell=True, check=True, stdout=subprocess.PIPE) @@ -46,6 +47,8 @@ def test_kubectl_ssl_verification(containerized_job): cert = subprocess.run(cmd.strip(), shell=True, check=True, stdout=subprocess.PIPE) cred.inputs['ssl_ca_cert'] = cert.stdout cred.save() - pm = PodManager(containerized_job) - ca_data = pm.kube_config['clusters'][0]['cluster']['certificate-authority-data'] + RunJob = namedtuple('RunJob', ['instance', 'build_execution_environment_params']) + rj = RunJob(instance=containerized_job, build_execution_environment_params=lambda x: {}) + receptor_job = AWXReceptorJob(rj, runner_params={'settings': {}}) + ca_data = receptor_job.kube_config['clusters'][0]['cluster']['certificate-authority-data'] assert cert.stdout == base64.b64decode(ca_data.encode()) From b73759e3802ee36a5dac6e07c84f94f301551de9 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 15:03:28 -0400 Subject: [PATCH 23/39] Fix collection test --- awx_collection/test/awx/conftest.py | 7 ++++++- awx_collection/test/awx/test_completeness.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 969bb96da0..4d09cb5930 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -16,7 +16,7 @@ from requests.models import Response, PreparedRequest import pytest from awx.main.tests.functional.conftest import _request -from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType +from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType, ExecutionEnvironment from django.db import transaction @@ -261,3 +261,8 @@ def silence_warning(): """Warnings use global variable, same as deprecations.""" with mock.patch('ansible.module_utils.basic.AnsibleModule.warn') as this_mock: yield this_mock + + +@pytest.fixture +def execution_environment(): + return ExecutionEnvironment.objects.create(name="test-ee", description="test-ee", managed_by_tower=True) diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 9deced2485..d639f828cd 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -157,7 +157,7 @@ def determine_state(module_id, endpoint, module, parameter, api_option, module_o return 'OK' -def test_completeness(collection_import, request, admin_user, job_template): +def test_completeness(collection_import, request, admin_user, job_template, execution_environment): option_comparison = {} # Load a list of existing module files from disk base_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) From ac41af8a5462d87f144ced0c402b9c57cb619deb Mon Sep 17 00:00:00 2001 From: nixocio Date: Wed, 24 Mar 2021 15:00:35 -0400 Subject: [PATCH 24/39] Add managed by tower as part of the EE details page Add managed by tower as part of the EE details page. See: https://github.com/ansible/awx/issues/8171 --- .../ExecutionEnvironmentDetails.jsx | 6 ++++++ .../ExecutionEnvironmentDetails.test.jsx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index a5c89f8d89..34de23c119 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -64,6 +64,11 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { value={description} dataCy="execution-environment-detail-description" /> + + ', () => { expect( wrapper.find('Detail[label="Credential"]').prop('value').props.children ).toEqual(executionEnvironment.summary_fields.credential.name); + expect( + wrapper.find('Detail[label="Managed by Tower"]').prop('value') + ).toEqual('False'); const dates = wrapper.find('UserDateDetail'); expect(dates).toHaveLength(2); expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created); @@ -167,6 +170,9 @@ describe('', () => { expect( wrapper.find('Detail[label="Credential"]').prop('value').props.children ).toEqual(executionEnvironment.summary_fields.credential.name); + expect( + wrapper.find('Detail[label="Managed by Tower"]').prop('value') + ).toEqual('True'); const dates = wrapper.find('UserDateDetail'); expect(dates).toHaveLength(2); expect(dates.at(0).prop('date')).toEqual(executionEnvironment.created); From b1119d2972d3a10db6b9f16c04785f7af3212c9c Mon Sep 17 00:00:00 2001 From: beeankha Date: Mon, 29 Mar 2021 16:11:06 -0400 Subject: [PATCH 25/39] Fix format specification linting erros --- awx_collection/plugins/module_utils/tower_api.py | 4 ++-- awx_collection/test/awx/test_module_utils.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index f6c63b08de..c637f0f7b7 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -266,8 +266,8 @@ class TowerAPIModule(TowerModule): collection_compare_ver = parsed_collection_version[0] tower_compare_ver = parsed_tower_version[0] else: - collection_compare_ver = "{}.{}".format(parsed_collection_version[0], parsed_collection_version[1]) - tower_compare_ver = '{}.{}'.format(parsed_tower_version[0], parsed_tower_version[1]) + collection_compare_ver = "{0}.{1}".format(parsed_collection_version[0], parsed_collection_version[1]) + tower_compare_ver = '{0}.{1}'.format(parsed_tower_version[0], parsed_tower_version[1]) if self._COLLECTION_TYPE not in self.collection_to_version or self.collection_to_version[self._COLLECTION_TYPE] != tower_type: self.warn("You are using the {0} version of this collection but connecting to {1}".format(self._COLLECTION_TYPE, tower_type)) diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 89bd44154e..a215db35fc 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -59,7 +59,7 @@ def test_version_warning(collection_import, silence_warning): my_module._COLLECTION_TYPE = "awx" my_module.get_endpoint('ping') silence_warning.assert_called_once_with( - 'You are running collection version {} but connecting to {} version {}'.format(my_module._COLLECTION_VERSION, awx_name, ping_version) + 'You are running collection version {0} but connecting to {1} version {2}'.format(my_module._COLLECTION_VERSION, awx_name, ping_version) ) @@ -107,7 +107,7 @@ def test_version_warning_strictness_tower(collection_import, silence_warning): my_module._COLLECTION_TYPE = "tower" my_module.get_endpoint('ping') silence_warning.assert_called_once_with( - 'You are running collection version {} but connecting to {} version {}'.format(my_module._COLLECTION_VERSION, tower_name, ping_version) + 'You are running collection version {0} but connecting to {1} version {2}'.format(my_module._COLLECTION_VERSION, tower_name, ping_version) ) @@ -121,7 +121,9 @@ def test_type_warning(collection_import, silence_warning): my_module._COLLECTION_VERSION = ping_version my_module._COLLECTION_TYPE = "tower" my_module.get_endpoint('ping') - silence_warning.assert_called_once_with('You are using the {} version of this collection but connecting to {}'.format(my_module._COLLECTION_TYPE, awx_name)) + silence_warning.assert_called_once_with( + 'You are using the {0} version of this collection but connecting to {1}'.format(my_module._COLLECTION_TYPE, awx_name) + ) def test_duplicate_config(collection_import, silence_warning): From 4beeeae9f1bef82aacb6152e84692eb55af1e235 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 29 Mar 2021 17:33:40 -0400 Subject: [PATCH 26/39] Fix k8s credentials that use a custom ca cert --- awx/main/models/credential/injectors.py | 2 +- awx/main/tests/unit/test_tasks.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index b5f7e37fed..246ab0d4e4 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -115,6 +115,6 @@ def kubernetes_bearer_token(cred, env, private_data_dir): with os.fdopen(handle, 'w') as f: os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) f.write(cred.get_input('ssl_ca_cert')) - env['K8S_AUTH_SSL_CA_CERT'] = path + env['K8S_AUTH_SSL_CA_CERT'] = os.path.join('/runner', os.path.basename(path)) else: env['K8S_AUTH_VERIFY_SSL'] = 'False' diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 26df22c4f2..5d600548a3 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1003,7 +1003,8 @@ class TestJobCredentials(TestJobExecution): if verify: assert env['K8S_AUTH_VERIFY_SSL'] == 'True' - cert = open(env['K8S_AUTH_SSL_CA_CERT'], 'r').read() + local_path = os.path.join(private_data_dir, os.path.basename(env['K8S_AUTH_SSL_CA_CERT'])) + cert = open(local_path, 'r').read() assert cert == 'CERTDATA' else: assert env['K8S_AUTH_VERIFY_SSL'] == 'False' From fce544bb73d79f643dabb69352188edd5480f323 Mon Sep 17 00:00:00 2001 From: sean-m-ssullivan Date: Mon, 29 Mar 2021 22:14:06 -0500 Subject: [PATCH 27/39] add test logic --- .../integration/targets/tower_project_update/tasks/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/awx_collection/tests/integration/targets/tower_project_update/tasks/main.yml b/awx_collection/tests/integration/targets/tower_project_update/tasks/main.yml index 08b9852018..dd614d552a 100644 --- a/awx_collection/tests/integration/targets/tower_project_update/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_project_update/tasks/main.yml @@ -53,6 +53,7 @@ - assert: that: - result is successful + - result is not changed - name: Delete the test project 1 tower_project: From 54308c5fa15b62373c5ee562efe7f634ed3ff94e Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 30 Mar 2021 09:08:39 -0400 Subject: [PATCH 28/39] Use Ansible Runner 2.0 alpha 1 --- requirements/requirements.in | 2 +- requirements/requirements.txt | 3 +-- requirements/requirements_git.txt | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 1970b215fb..50ee011690 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -1,5 +1,5 @@ aiohttp -ansible-runner>=1.4.7 +ansible-runner==2.0.0a1 ansiconv==1.0.0 # UPGRADE BLOCKER: from 2013, consider replacing instead of upgrading asciichartpy autobahn>=20.12.3 # CVE-2020-35678 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9450d4f879..0ebb97cb02 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,10 +4,9 @@ aiohttp==3.6.2 # via -r /awx_devel/requirements/requirements.in aioredis==1.3.1 # via channels-redis -#ansible-runner==1.4.7 +ansible-runner==2.0.0a1 # via # -r /awx_devel/requirements/requirements.in - # -r /awx_devel/requirements/requirements_git.txt ansiconv==1.0.0 # via -r /awx_devel/requirements/requirements.in asciichartpy==1.5.25 diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index f2f3abaa7a..be9973c1f8 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1,3 +1,2 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi -git+git://github.com/ansible/ansible-runner@devel#egg=ansible-runner git+https://github.com/project-receptor/receptor.git@0.9.6#egg=receptorctl&subdirectory=receptorctl From dde408ea1a275171fbb4098ddfa78af387314cfa Mon Sep 17 00:00:00 2001 From: sean-m-ssullivan Date: Tue, 30 Mar 2021 09:35:52 -0500 Subject: [PATCH 29/39] update docs --- awx_collection/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx_collection/README.md b/awx_collection/README.md index 61480a1b3d..506d00e4a3 100644 --- a/awx_collection/README.md +++ b/awx_collection/README.md @@ -72,6 +72,8 @@ Notable releases of the `awx.awx` collection: The following notes are changes that may require changes to playbooks: - When a project is created, it will wait for the update/sync to finish by default; this can be turned off with the `wait` parameter, if desired. + - When using the wait parameter with project update, if the project did not undergo a revision update, the result will be + 'not changed' - Creating a "scan" type job template is no longer supported. - Specifying a custom certificate via the `TOWER_CERTIFICATE` environment variable no longer works. - Type changes of variable fields: From 4a726b7f6f2ca496e4b5e80df7551097d81bacf3 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Tue, 30 Mar 2021 13:33:17 -0400 Subject: [PATCH 30/39] Fix race condition that causes InvalidGitRepositoryError --- awx/main/tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 221a9ce600..992620fbd8 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1807,13 +1807,14 @@ class RunJob(BaseTask): logger.debug('Performing fresh clone of {} on this instance.'.format(job.project)) sync_needs.append(source_update_tag) elif job.project.scm_type == 'git' and job.project.scm_revision and (not branch_override): - git_repo = git.Repo(project_path) try: + git_repo = git.Repo(project_path) + if job_revision == git_repo.head.commit.hexsha: logger.debug('Skipping project sync for {} because commit is locally available'.format(job.log_format)) else: sync_needs.append(source_update_tag) - except (ValueError, BadGitName): + except (ValueError, BadGitName, git.exc.InvalidGitRepositoryError): logger.debug('Needed commit for {} not in local source tree, will sync with remote'.format(job.log_format)) sync_needs.append(source_update_tag) else: From 6047eb61721abdd91183047bbf78618fe5590fa0 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 31 Mar 2021 12:27:01 -0400 Subject: [PATCH 31/39] Fix rounding error in output pagination controls --- .../src/screens/Job/JobOutput/shared/jobOutputUtils.js | 2 +- .../screens/Job/JobOutput/shared/jobOutputUtils.test.jsx | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.js b/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.js index cb3e7fb309..e61afeca32 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.js +++ b/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.js @@ -6,7 +6,7 @@ export default function getRowRangePageSize(startIndex, stopIndex) { page = startIndex + 1; pageSize = 1; } else if (stopIndex >= startIndex + 50) { - page = Math.ceil(startIndex / 50); + page = Math.floor(startIndex / 50) + 1; pageSize = 50; } else { for (let i = stopIndex - startIndex + 1; i <= 50; i++) { diff --git a/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.test.jsx index 2c06e347ba..ba4a0e9844 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.test.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/shared/jobOutputUtils.test.jsx @@ -29,4 +29,11 @@ describe('getRowRangePageSize', () => { firstIndex: 5, }); }); + test('handles range with 0 startIndex', () => { + expect(getRowRangePageSize(0, 50)).toEqual({ + page: 1, + pageSize: 50, + firstIndex: 0, + }); + }); }); From c39a4051d72efe0300ddc91440ecffb6afe10650 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 31 Mar 2021 14:00:28 -0400 Subject: [PATCH 32/39] Use receptorctl from pypi --- docs/licenses/{receptor.txt => receptorctl.txt} | 0 requirements/requirements.in | 2 ++ requirements/requirements.txt | 11 +++++------ requirements/requirements_git.txt | 1 - 4 files changed, 7 insertions(+), 7 deletions(-) rename docs/licenses/{receptor.txt => receptorctl.txt} (100%) diff --git a/docs/licenses/receptor.txt b/docs/licenses/receptorctl.txt similarity index 100% rename from docs/licenses/receptor.txt rename to docs/licenses/receptorctl.txt diff --git a/requirements/requirements.in b/requirements/requirements.in index 50ee011690..ef33a58e8b 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -37,11 +37,13 @@ openshift>=0.11.0 # minimum version to pull in new pyyaml for CVE-2017-18342 pexpect==4.7.0 # see library notes prometheus_client psycopg2 +psutil pygerduty pyparsing python3-saml python-ldap>=3.3.1 # https://github.com/python-ldap/python-ldap/issues/270 pyyaml>=5.4.1 # minimum to fix https://github.com/yaml/pyyaml/issues/478 +receptorctl schedule==0.6.0 social-auth-core==3.3.1 # see UPGRADE BLOCKERs social-auth-app-django==3.1.0 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0ebb97cb02..b7386b30d9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,8 +5,7 @@ aiohttp==3.6.2 aioredis==1.3.1 # via channels-redis ansible-runner==2.0.0a1 - # via - # -r /awx_devel/requirements/requirements.in + # via -r /awx_devel/requirements/requirements.in ansiconv==1.0.0 # via -r /awx_devel/requirements/requirements.in asciichartpy==1.5.25 @@ -230,8 +229,8 @@ pkgconfig==1.5.1 # via xmlsec prometheus-client==0.7.1 # via -r /awx_devel/requirements/requirements.in -psutil==5.7.0 - # via ansible-runner +psutil==5.8.0 + # via -r /awx_devel/requirements/requirements.in psycopg2==2.8.4 # via -r /awx_devel/requirements/requirements.in ptyprocess==0.6.0 @@ -296,7 +295,8 @@ pyyaml==5.4.1 # djangorestframework-yaml # kubernetes # receptorctl - # via -r /awx_devel/requirements/requirements_git.txt +receptorctl==0.9.7 + # via -r /awx_devel/requirements/requirements.in redis==3.4.1 # via # -r /awx_devel/requirements/requirements.in @@ -411,5 +411,4 @@ setuptools==41.6.0 # kubernetes # markdown # python-daemon - # receptorctl # zope.interface diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index be9973c1f8..340cbfdcc7 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1,2 +1 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi -git+https://github.com/project-receptor/receptor.git@0.9.6#egg=receptorctl&subdirectory=receptorctl From 5180fccd12f3936559411083dd2ab2366f5c92e8 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 31 Mar 2021 16:20:26 -0400 Subject: [PATCH 33/39] Update receptor version used in dev environment --- tools/ansible/roles/dockerfile/templates/Dockerfile.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 8feb7e90dd..617f4e9200 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -170,7 +170,7 @@ COPY --from=builder /var/lib/awx /var/lib/awx RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage {%if build_dev|bool %} -COPY --from=quay.io/project-receptor/receptor:0.9.6 /usr/bin/receptor /usr/bin/receptor +COPY --from=quay.io/project-receptor/receptor:0.9.7 /usr/bin/receptor /usr/bin/receptor 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 && \ From 1a8a137e9cb7645f4301dc9ecb1608be13c6d9a0 Mon Sep 17 00:00:00 2001 From: Yanis Guenane Date: Thu, 1 Apr 2021 14:19:21 +0200 Subject: [PATCH 34/39] Add wheel in venv creation The generated based venv from `python3.8 -m venv` vs. `virtualenv -p python38` is different. This changes aims to address the differences. It was introduced as part of the Python 3.8 migration. https://github.com/ansible/awx/pull/8778 --- Makefile | 2 +- docs/licenses/Cython.txt | 176 ++++++++++++++++++++++++++++++++++ docs/licenses/wheel.txt | 22 +++++ requirements/requirements.in | 2 + requirements/requirements.txt | 5 + 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 docs/licenses/Cython.txt create mode 100644 docs/licenses/wheel.txt diff --git a/Makefile b/Makefile index e2cac573bd..0bcfb5f9c8 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ DEVEL_IMAGE_NAME ?= $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG) SRC_ONLY_PKGS ?= cffi,pycparser,psycopg2,twilio,pycurl # These should be upgraded in the AWX and Ansible venv before attempting # to install the actual requirements -VENV_BOOTSTRAP ?= pip==19.3.1 setuptools==41.6.0 +VENV_BOOTSTRAP ?= pip==19.3.1 setuptools==41.6.0 wheel==0.36.2 # Determine appropriate shasum command UNAME_S := $(shell uname -s) diff --git a/docs/licenses/Cython.txt b/docs/licenses/Cython.txt new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/docs/licenses/Cython.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/docs/licenses/wheel.txt b/docs/licenses/wheel.txt new file mode 100644 index 0000000000..c3441e6cc8 --- /dev/null +++ b/docs/licenses/wheel.txt @@ -0,0 +1,22 @@ +"wheel" copyright (c) 2012-2014 Daniel Holth and +contributors. + +The MIT License + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/requirements/requirements.in b/requirements/requirements.in index ef33a58e8b..0868e7979a 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -7,6 +7,7 @@ azure-keyvault==1.1.0 # see UPGRADE BLOCKERs channels channels-redis>=3.1.0 # https://github.com/django/channels_redis/issues/212 cryptography<3.0.0 +Cython<3 # Since the bump to PyYAML 5.4.1 this is now a mandatory dep daphne distro django==2.2.16 # see UPGRADE BLOCKERs @@ -55,5 +56,6 @@ twilio twisted[tls]>=20.3.0 # CVE-2020-10108, CVE-2020-10109 uWSGI uwsgitop +wheel pip==19.3.1 # see UPGRADE BLOCKERs setuptools==41.6.0 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b7386b30d9..c9b4958537 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -70,6 +70,9 @@ cryptography==2.9.2 # pyopenssl # service-identity # social-auth-core +Cython==0.29.22 + # via + # -r /awx_devel/requirements/requirements.in daphne==2.4.1 # via # -r /awx_devel/requirements/requirements.in @@ -412,3 +415,5 @@ setuptools==41.6.0 # markdown # python-daemon # zope.interface +wheel==0.36.2 + # via -r /awx_devel/requirements/requirements.in From 225c3263d071a63e1f3d63396933a3dd6b396ed8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 29 Mar 2021 14:03:47 -0400 Subject: [PATCH 35/39] add convergence to workflows --- .../components/Workflow/workflowReducer.js | 12 +- .../Job/WorkflowOutput/WorkflowOutputNode.jsx | 29 +++ .../Modals/NodeModals/NodeAddModal.jsx | 4 + .../Modals/NodeModals/NodeAddModal.test.jsx | 1 + .../Modals/NodeModals/NodeEditModal.jsx | 3 + .../Modals/NodeModals/NodeEditModal.test.jsx | 1 + .../Modals/NodeModals/NodeModal.jsx | 2 +- .../Modals/NodeModals/NodeModal.test.jsx | 7 + .../NodeModals/NodeTypeStep/NodeTypeStep.jsx | 176 ++++++++++++------ .../NodeTypeStep/NodeTypeStep.test.jsx | 1 + .../NodeTypeStep/useNodeTypeStep.jsx | 1 + .../Modals/NodeModals/NodeViewModal.test.jsx | 1 + .../Modals/NodeModals/useWorkflowNodeSteps.js | 11 +- .../Visualizer.jsx | 97 ++++++---- .../Visualizer.test.jsx | 13 ++ .../VisualizerNode.jsx | 38 ++++ 16 files changed, 298 insertions(+), 99 deletions(-) diff --git a/awx/ui_next/src/components/Workflow/workflowReducer.js b/awx/ui_next/src/components/Workflow/workflowReducer.js index 6d5e5bf635..bb221cdbcb 100644 --- a/awx/ui_next/src/components/Workflow/workflowReducer.js +++ b/awx/ui_next/src/components/Workflow/workflowReducer.js @@ -183,6 +183,7 @@ function createNode(state, node) { fullUnifiedJobTemplate: node.nodeResource, isInvalidLinkTarget: false, promptValues: node.promptValues, + all_parents_must_converge: node.all_parents_must_converge, }); // Ensures that root nodes appear to always run @@ -657,10 +658,19 @@ function updateLink(state, linkType) { function updateNode(state, editedNode) { const { nodeToEdit, nodes } = state; - const { nodeResource, launchConfig, promptValues } = editedNode; + const { + nodeResource, + launchConfig, + promptValues, + all_parents_must_converge, + } = editedNode; const newNodes = [...nodes]; const matchingNode = newNodes.find(node => node.id === nodeToEdit.id); + matchingNode.all_parents_must_converge = all_parents_must_converge; + if (matchingNode.originalNodeObject) { + delete matchingNode.originalNodeObject.all_parents_must_converge; + } matchingNode.fullUnifiedJobTemplate = nodeResource; matchingNode.isEdited = true; matchingNode.launchConfig = launchConfig; diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx index 7ae47b6c72..85c1603493 100644 --- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx +++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx @@ -59,6 +59,11 @@ const NodeDefaultLabel = styled.p` white-space: nowrap; `; +const ConvergenceLabel = styled.p` + font-size: 12px; + color: #ffffff; +`; + Elapsed.displayName = 'Elapsed'; function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { @@ -100,6 +105,30 @@ function WorkflowOutputNode({ i18n, mouseEnter, mouseLeave, node }) { onMouseEnter={mouseEnter} onMouseLeave={mouseLeave} > + {(node.all_parents_must_converge || + node?.originalNodeObject?.all_parents_must_converge) && ( + <> + + + {i18n._(t`ALL`)} + + + )} { expect(dispatch).toHaveBeenCalledWith({ node: { + all_parents_must_converge: false, linkType: 'success', nodeResource: { id: 448, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx index 428986e2fe..8298294324 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.jsx @@ -17,11 +17,13 @@ function NodeEditModal({ i18n }) { nodeType, timeoutMinutes, timeoutSeconds, + convergence, ...rest } = values; let node; if (values.nodeType === 'workflow_approval_template') { node = { + all_parents_must_converge: convergence === 'all', nodeResource: { description: approvalDescription, name: approvalName, @@ -32,6 +34,7 @@ function NodeEditModal({ i18n }) { } else { node = { nodeResource, + all_parents_must_converge: convergence === 'all', }; if (nodeType === 'job_template' || nodeType === 'workflow_job_template') { node.promptValues = { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.test.jsx index 08b046d27b..2a9ee455b7 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeEditModal.test.jsx @@ -63,6 +63,7 @@ describe('NodeEditModal', () => { }); expect(dispatch).toHaveBeenCalledWith({ node: { + all_parents_must_converge: false, nodeResource: { id: 448, name: 'Test JT', type: 'job_template' }, }, type: 'UPDATE_NODE', diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx index 9883ccec19..631ee9cc31 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.jsx @@ -101,7 +101,6 @@ function NodeModalForm({ values.extra_data = extraVars && parseVariableField(extraVars); delete values.extra_vars; } - onSave(values, launchConfig); }; @@ -357,6 +356,7 @@ const NodeModal = ({ onSave, i18n, askLinkType, title }) => { approvalDescription: '', timeoutMinutes: 0, timeoutSeconds: 0, + convergence: 'any', linkType: 'success', nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null, nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template', diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx index 0123f66518..b07af7268b 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeModal.test.jsx @@ -307,6 +307,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', linkType: 'always', nodeType: 'job_template', inventory: { name: 'Foo Inv', id: 1 }, @@ -345,6 +346,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', linkType: 'failure', nodeResource: { id: 1, @@ -383,6 +385,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', linkType: 'failure', nodeResource: { id: 1, @@ -422,6 +425,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', linkType: 'success', nodeResource: { id: 1, @@ -506,6 +510,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', approvalDescription: 'Test Approval Description', approvalName: 'Test Approval', linkType: 'always', @@ -605,6 +610,7 @@ describe('NodeModal', () => { expect(onSave).toBeCalledWith( { + convergence: 'any', approvalDescription: 'Test Approval Description', approvalName: 'Test Approval', linkType: 'success', @@ -668,6 +674,7 @@ describe('NodeModal', () => { }); expect(onSave).toBeCalledWith( { + convergence: 'any', linkType: 'success', nodeResource: { id: 1, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx index 8b653582a9..028db20329 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx @@ -1,13 +1,25 @@ import 'styled-components/macro'; -import React from 'react'; +import React, { useState } from 'react'; import { withI18n } from '@lingui/react'; import { t, Trans } from '@lingui/macro'; import styled from 'styled-components'; import { useField } from 'formik'; -import { Alert, Form, FormGroup, TextInput } from '@patternfly/react-core'; +import { + Alert, + Form, + FormGroup, + TextInput, + Select, + SelectVariant, + SelectOption, +} from '@patternfly/react-core'; import { required } from '../../../../../../util/validators'; -import { FormFullWidthLayout } from '../../../../../../components/FormLayout'; +import { + FormColumnLayout, + FormFullWidthLayout, +} from '../../../../../../components/FormLayout'; +import Popover from '../../../../../../components/Popover'; import AnsibleSelect from '../../../../../../components/AnsibleSelect'; import InventorySourcesList from './InventorySourcesList'; import JobTemplatesList from './JobTemplatesList'; @@ -44,6 +56,9 @@ function NodeTypeStep({ i18n }) { const [timeoutSecondsField, , timeoutSecondsHelpers] = useField( 'timeoutSeconds' ); + const [convergenceField, , convergenceFieldHelpers] = useField('convergence'); + + const [isConvergenceOpen, setIsConvergenceOpen] = useState(false); const isValid = !approvalNameMeta.touched || !approvalNameMeta.error; return ( @@ -101,6 +116,7 @@ function NodeTypeStep({ i18n }) { approvalDescriptionHelpers.setValue(''); timeoutMinutesHelpers.setValue(0); timeoutSecondsHelpers.setValue(0); + convergenceFieldHelpers.setValue('any'); }} /> @@ -129,61 +145,107 @@ function NodeTypeStep({ i18n }) { onUpdateNodeResource={nodeResourceHelpers.setValue} /> )} - {nodeTypeField.value === 'workflow_approval_template' && ( -
- - - - + + {nodeTypeField.value === 'workflow_approval_template' && ( + + + + +
+ { + timeoutMinutesField.onChange(event); + }} + step="1" + type="number" + /> + + min + + { + timeoutSecondsField.onChange(event); + }} + step="1" + type="number" + /> + + sec + +
+
+
+ )} + + {i18n._( + t`Preconditions for running this node when there are multiple parents. Refer to the` + )}{' '} + + {i18n._(t`documentation`)} + {' '} + {i18n._(t`for more info.`)} + + } + /> + } + > + + +
+ ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx index 580ba1ee1e..235544486f 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.test.jsx @@ -177,6 +177,7 @@ describe('NodeTypeStep', () => { approvalDescription: '', timeoutMinutes: 0, timeoutSeconds: 0, + convergence: 'any', }} > diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/useNodeTypeStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/useNodeTypeStep.jsx index d7c83097e9..5931d2041b 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/useNodeTypeStep.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/useNodeTypeStep.jsx @@ -86,5 +86,6 @@ function getInitialValues() { timeoutMinutes: 0, timeoutSeconds: 0, nodeType: 'job_template', + convergence: 'any', }; } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx index 8af36ff31f..03eed26ad0 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeViewModal.test.jsx @@ -282,6 +282,7 @@ describe('NodeViewModal', () => { description: '', type: 'workflow_approval_template', timeout: 0, + all_parents_must_converge: false, }, }, }; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js index e882a93329..9d60376c64 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/useWorkflowNodeSteps.js @@ -39,6 +39,11 @@ const getNodeToEditDefaultValues = ( const initialValues = { nodeResource: nodeToEdit?.fullUnifiedJobTemplate || null, nodeType: nodeToEdit?.fullUnifiedJobTemplate?.type || 'job_template', + convergence: + nodeToEdit?.all_parents_must_converge || + nodeToEdit?.originalNodeObject?.all_parents_must_converge + ? 'all' + : 'any', }; if ( @@ -228,7 +233,6 @@ export default function useWorkflowNodeSteps( useEffect(() => { if (launchConfig && surveyConfig && isReady) { let initialValues = {}; - if ( nodeToEdit && nodeToEdit?.fullUnifiedJobTemplate && @@ -264,10 +268,15 @@ export default function useWorkflowNodeSteps( ); } + if (initialValues.convergence === 'all') { + formikValues.convergence = 'all'; + } + resetForm({ errors, values: { ...initialValues, + convergence: formikValues.convergence, nodeResource: formikValues.nodeResource, nodeType: formikValues.nodeType, linkType: formikValues.linkType, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx index 5e0b8e2006..b94bd7ac44 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx @@ -369,27 +369,24 @@ function Visualizer({ template, i18n }) { node.fullUnifiedJobTemplate.type === 'workflow_approval_template' ) { nodeRequests.push( - WorkflowJobTemplatesAPI.createNode(template.id, {}).then( - ({ data }) => { - node.originalNodeObject = data; - originalLinkMap[node.id] = { - id: data.id, - success_nodes: [], - failure_nodes: [], - always_nodes: [], - }; - approvalTemplateRequests.push( - WorkflowJobTemplateNodesAPI.createApprovalTemplate( - data.id, - { - name: node.fullUnifiedJobTemplate.name, - description: node.fullUnifiedJobTemplate.description, - timeout: node.fullUnifiedJobTemplate.timeout, - } - ) - ); - } - ) + WorkflowJobTemplatesAPI.createNode(template.id, { + all_parents_must_converge: node.all_parents_must_converge, + }).then(({ data }) => { + node.originalNodeObject = data; + originalLinkMap[node.id] = { + id: data.id, + success_nodes: [], + failure_nodes: [], + always_nodes: [], + }; + approvalTemplateRequests.push( + WorkflowJobTemplateNodesAPI.createApprovalTemplate(data.id, { + name: node.fullUnifiedJobTemplate.name, + description: node.fullUnifiedJobTemplate.description, + timeout: node.fullUnifiedJobTemplate.timeout, + }) + ); + }) ); } else { nodeRequests.push( @@ -397,6 +394,7 @@ function Visualizer({ template, i18n }) { ...node.promptValues, inventory: node.promptValues?.inventory?.id || null, unified_job_template: node.fullUnifiedJobTemplate.id, + all_parents_must_converge: node.all_parents_must_converge, }).then(({ data }) => { node.originalNodeObject = data; originalLinkMap[node.id] = { @@ -427,27 +425,47 @@ function Visualizer({ template, i18n }) { node.originalNodeObject.summary_fields.unified_job_template .unified_job_type === 'workflow_approval' ) { - approvalTemplateRequests.push( - WorkflowApprovalTemplatesAPI.update( - node.originalNodeObject.summary_fields.unified_job_template - .id, - { - name: node.fullUnifiedJobTemplate.name, - description: node.fullUnifiedJobTemplate.description, - timeout: node.fullUnifiedJobTemplate.timeout, - } - ) - ); - } else { - approvalTemplateRequests.push( - WorkflowJobTemplateNodesAPI.createApprovalTemplate( + nodeRequests.push( + WorkflowJobTemplateNodesAPI.replace( node.originalNodeObject.id, { - name: node.fullUnifiedJobTemplate.name, - description: node.fullUnifiedJobTemplate.description, - timeout: node.fullUnifiedJobTemplate.timeout, + all_parents_must_converge: node.all_parents_must_converge, } - ) + ).then(({ data }) => { + node.originalNodeObject = data; + approvalTemplateRequests.push( + WorkflowApprovalTemplatesAPI.update( + node.originalNodeObject.summary_fields + .unified_job_template.id, + { + name: node.fullUnifiedJobTemplate.name, + description: node.fullUnifiedJobTemplate.description, + timeout: node.fullUnifiedJobTemplate.timeout, + } + ) + ); + }) + ); + } else { + nodeRequests.push( + WorkflowJobTemplateNodesAPI.replace( + node.originalNodeObject.id, + { + all_parents_must_converge: node.all_parents_must_converge, + } + ).then(({ data }) => { + node.originalNodeObject = data; + approvalTemplateRequests.push( + WorkflowJobTemplateNodesAPI.createApprovalTemplate( + node.originalNodeObject.id, + { + name: node.fullUnifiedJobTemplate.name, + description: node.fullUnifiedJobTemplate.description, + timeout: node.fullUnifiedJobTemplate.timeout, + } + ) + ); + }) ); } } else { @@ -456,6 +474,7 @@ function Visualizer({ template, i18n }) { ...node.promptValues, inventory: node.promptValues?.inventory?.id || null, unified_job_template: node.fullUnifiedJobTemplate.id, + all_parents_must_converge: node.all_parents_must_converge, }).then(() => { const { added: addedCredentials, diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx index c9a5795929..dcd0c64524 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.test.jsx @@ -419,6 +419,7 @@ describe('Visualizer', () => { ).toBe(1); }); + // TODO: figure out why this test is failing, the scenario passes in the ui test('Error shown when saving fails due to approval template edit error', async () => { workflowReducer.mockImplementation(state => { const newState = { @@ -459,6 +460,17 @@ describe('Visualizer', () => { results: [], }, }); + WorkflowJobTemplateNodesAPI.replace.mockResolvedValue({ + data: { + id: 9000, + summary_fields: { + unified_job_template: { + unified_job_type: 'workflow_approval', + id: 1, + }, + }, + }, + }); WorkflowApprovalTemplatesAPI.update.mockRejectedValue(new Error()); await act(async () => { wrapper = mountWithContexts( @@ -475,6 +487,7 @@ describe('Visualizer', () => { wrapper.find('Button#visualizer-save').simulate('click'); }); wrapper.update(); + expect(WorkflowJobTemplateNodesAPI.replace).toHaveBeenCalledTimes(1); expect(WorkflowApprovalTemplatesAPI.update).toHaveBeenCalledTimes(1); expect( wrapper.find('AlertModal[title="Error saving the workflow!"]').length diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerNode.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerNode.jsx index 65bd1c5008..a92be897f8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerNode.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerNode.jsx @@ -44,6 +44,12 @@ const NodeResourceName = styled.p` text-overflow: ellipsis; white-space: nowrap; `; + +const ConvergenceLabel = styled.p` + font-size: 12px; + color: #ffffff; +`; + NodeResourceName.displayName = 'NodeResourceName'; function VisualizerNode({ @@ -244,6 +250,38 @@ function VisualizerNode({ node.id ].y - nodePositions[1].y})`} > + {(node.all_parents_must_converge || + node?.originalNodeObject?.all_parents_must_converge) && ( + <> + + + {i18n._(t`ALL`)} + + + )} Date: Thu, 1 Apr 2021 13:06:25 -0400 Subject: [PATCH 36/39] update selectos for convergence --- .../Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx index 028db20329..fcc1170470 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx @@ -234,7 +234,8 @@ function NodeTypeStep({ i18n }) { setIsConvergenceOpen(false); }} aria-label={i18n._(t`Convergence select`)} - id="convergence-select" + className="convergenceSelect" + ouiaId="convergenceSelect" > {i18n._(t`Any`)} From e90f720153be45b99bcc15a7ca8292e528846a05 Mon Sep 17 00:00:00 2001 From: John Hill Date: Thu, 1 Apr 2021 15:08:14 -0400 Subject: [PATCH 37/39] Attempt at adding locators for select options --- .../Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx index fcc1170470..8478305ed4 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/NodeTypeStep.jsx @@ -237,10 +237,10 @@ function NodeTypeStep({ i18n }) { className="convergenceSelect" ouiaId="convergenceSelect" > - + {i18n._(t`Any`)} - + {i18n._(t`All`)} From f89cf95c5184abdd036538e8ab1c2677f11eee36 Mon Sep 17 00:00:00 2001 From: Yanis Guenane Date: Tue, 23 Mar 2021 10:30:50 +0100 Subject: [PATCH 38/39] Rename ansible-tower file to automation-controller --- MANIFEST.in | 2 +- setup.py | 2 +- tools/scripts/ansible-tower-service | 18 ------------------ tools/scripts/automation-controller-service | 18 ++++++++++++++++++ 4 files changed, 20 insertions(+), 20 deletions(-) delete mode 100755 tools/scripts/ansible-tower-service create mode 100755 tools/scripts/automation-controller-service diff --git a/MANIFEST.in b/MANIFEST.in index 1466361602..72c0f4de21 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,7 +20,7 @@ recursive-exclude awx/ui/client * recursive-exclude awx/settings local_settings.py* include tools/scripts/request_tower_configuration.sh include tools/scripts/request_tower_configuration.ps1 -include tools/scripts/ansible-tower-service +include tools/scripts/automation-controller-service include tools/scripts/failure-event-handler include tools/scripts/awx-python include awx/playbooks/library/mkfifo.py diff --git a/setup.py b/setup.py index 55fcff6785..f0c8a130f6 100755 --- a/setup.py +++ b/setup.py @@ -141,7 +141,7 @@ setup( # ("%s" % webconfig, ["config/uwsgi_params"]), ("%s" % sharedir, ["tools/scripts/request_tower_configuration.sh","tools/scripts/request_tower_configuration.ps1"]), ("%s" % docdir, ["docs/licenses/*",]), - ("%s" % bindir, ["tools/scripts/ansible-tower-service", + ("%s" % bindir, ["tools/scripts/automation-controller-service", "tools/scripts/failure-event-handler", "tools/scripts/awx-python", "tools/scripts/ansible-tower-setup"]), diff --git a/tools/scripts/ansible-tower-service b/tools/scripts/ansible-tower-service deleted file mode 100755 index 68ff85463d..0000000000 --- a/tools/scripts/ansible-tower-service +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -if [ -f /etc/sysconfig/ansible-tower ]; then - source /etc/sysconfig/ansible-tower -fi - -case "$1" in - start|stop|restart) - exec systemctl $1 ansible-tower.service - ;; - status) - exec systemctl status ansible-tower.service $TOWER_SERVICES - ;; - *) - echo "Usage: ansible-tower-service start|stop|restart|status" - exit 1 - ;; -esac diff --git a/tools/scripts/automation-controller-service b/tools/scripts/automation-controller-service new file mode 100755 index 0000000000..097879a9f0 --- /dev/null +++ b/tools/scripts/automation-controller-service @@ -0,0 +1,18 @@ +#!/bin/bash + +if [ -f /etc/sysconfig/automation-controller ]; then + source /etc/sysconfig/automation-controller +fi + +case "$1" in + start|stop|restart) + exec systemctl $1 automation-controller.service + ;; + status) + exec systemctl status automation-controller.service $TOWER_SERVICES + ;; + *) + echo "Usage: automation-controller-service start|stop|restart|status" + exit 1 + ;; +esac From fccfef442c22a99ac2ac439b2dc6ed25bc2cfa0f Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Fri, 2 Apr 2021 14:16:51 -0400 Subject: [PATCH 39/39] Make logs more readable in development environment This uses https://github.com/coderanger/supervisor-stdout to prefix process names before log messages in the dev env --- .../roles/dockerfile/templates/Dockerfile.j2 | 4 +- tools/docker-compose/supervisor.conf | 39 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index 617f4e9200..bbdcfadcf5 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -119,7 +119,7 @@ RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ chmod +x /usr/bin/tini -RUN python3.8 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}black{% endif %} +RUN python3.8 -m ensurepip && pip3 install "virtualenv < 20" supervisor RUN rm -rf /root/.cache && rm -rf /tmp/* @@ -153,6 +153,8 @@ RUN dnf -y install \ unzip && \ npm install -g n && n 14.15.1 && dnf remove -y nodejs +RUN pip3 install black git+https://github.com/coderanger/supervisor-stdout + # This package randomly fails to download. # It is nice to have in the dev env, but not necessary. # Add it back to the list above if the repo ever straighten up. diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf index fc2eb2d028..9d0cd4ded3 100644 --- a/tools/docker-compose/supervisor.conf +++ b/tools/docker-compose/supervisor.conf @@ -12,8 +12,9 @@ stopsignal=KILL stopasgroup=true killasgroup=true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true + [program:awx-receiver] command = make receiver @@ -24,8 +25,8 @@ stopsignal=KILL stopasgroup=true killasgroup=true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-wsbroadcast] command = make wsbroadcast @@ -36,8 +37,8 @@ stopsignal=KILL stopasgroup=true killasgroup=true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-uwsgi] command = make uwsgi @@ -48,8 +49,8 @@ stopwaitsecs = 1 stopsignal=KILL stopasgroup=true killasgroup=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-daphne] command = make daphne @@ -60,16 +61,16 @@ stopwaitsecs = 1 stopsignal=KILL stopasgroup=true killasgroup=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-nginx] command = make nginx autostart = true autorestart = true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-rsyslogd] command = rsyslogd -n -i /var/run/awx-rsyslog/rsyslog.pid -f /var/lib/awx/rsyslog/rsyslog.conf @@ -80,8 +81,8 @@ stopsignal=TERM stopasgroup=true killasgroup=true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [program:awx-receptor] command = receptor --config /etc/receptor/receptor.conf @@ -91,8 +92,8 @@ stopsignal = KILL stopasgroup = true killasgroup = true redirect_stderr=true -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 +stdout_events_enabled = true +stderr_events_enabled = true [group:tower-processes] programs=awx-dispatcher,awx-receiver,awx-uwsgi,awx-daphne,awx-nginx,awx-wsbroadcast,awx-rsyslogd @@ -106,3 +107,9 @@ serverurl=unix:///var/run/supervisor/supervisor.sock ; use a unix:// URL for a [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[eventlistener:stdout] +command = supervisor_stdout +buffer_size = 100 +events = PROCESS_LOG +result_handler = supervisor_stdout:event_handler