From 2bf03a87b96267d4b90da9b1af053a9ad4dc9af1 Mon Sep 17 00:00:00 2001 From: Chad Ferman Date: Fri, 14 Jun 2024 12:38:37 -0500 Subject: [PATCH] Add OpenShift Virtualization Inventory source option (#15047) Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> --- awx/main/constants.py | 2 +- ...4_alter_inventorysource_source_and_more.py | 61 +++++++++++++++++ awx/main/models/inventory.py | 17 ++++- .../plugins/openshift_virtualization/env.json | 5 ++ .../test_inventory_source_injectors.py | 2 + awx/settings/defaults.py | 5 ++ .../InventorySourceAdd.test.js | 4 ++ .../Inventory/shared/Inventory.helptext.js | 6 +- .../Inventory/shared/InventorySourceForm.js | 10 +++ .../OpenShiftVirtualizationSubForm.js | 64 ++++++++++++++++++ .../OpenShiftVirtualizationSubForm.test.js | 65 +++++++++++++++++++ .../shared/InventorySourceSubForms/index.js | 1 + .../plugins/modules/inventory_source.py | 20 +++++- 13 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 awx/main/migrations/0194_alter_inventorysource_source_and_more.py create mode 100644 awx/main/tests/data/inventory/plugins/openshift_virtualization/env.json create mode 100644 awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.js create mode 100644 awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.test.js diff --git a/awx/main/constants.py b/awx/main/constants.py index 115b0626043d..a12da39938fc 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -14,7 +14,7 @@ 'STANDARD_INVENTORY_UPDATE_ENV', ] -CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'controller', 'insights', 'terraform') +CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'rhv', 'satellite6', 'controller', 'insights', 'terraform', 'openshift_virtualization') PRIVILEGE_ESCALATION_METHODS = [ ('sudo', _('Sudo')), ('su', _('Su')), diff --git a/awx/main/migrations/0194_alter_inventorysource_source_and_more.py b/awx/main/migrations/0194_alter_inventorysource_source_and_more.py new file mode 100644 index 000000000000..d6f399d71c5a --- /dev/null +++ b/awx/main/migrations/0194_alter_inventorysource_source_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.10 on 2024-06-12 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0193_alter_notification_notification_type_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='inventorysource', + name='source', + field=models.CharField( + choices=[ + ('file', 'File, Directory or Script'), + ('constructed', 'Template additional groups and hostvars at runtime'), + ('scm', 'Sourced from a Project'), + ('ec2', 'Amazon EC2'), + ('gce', 'Google Compute Engine'), + ('azure_rm', 'Microsoft Azure Resource Manager'), + ('vmware', 'VMware vCenter'), + ('satellite6', 'Red Hat Satellite 6'), + ('openstack', 'OpenStack'), + ('rhv', 'Red Hat Virtualization'), + ('controller', 'Red Hat Ansible Automation Platform'), + ('insights', 'Red Hat Insights'), + ('terraform', 'Terraform State'), + ('openshift_virtualization', 'OpenShift Virtualization'), + ], + default=None, + max_length=32, + ), + ), + migrations.AlterField( + model_name='inventoryupdate', + name='source', + field=models.CharField( + choices=[ + ('file', 'File, Directory or Script'), + ('constructed', 'Template additional groups and hostvars at runtime'), + ('scm', 'Sourced from a Project'), + ('ec2', 'Amazon EC2'), + ('gce', 'Google Compute Engine'), + ('azure_rm', 'Microsoft Azure Resource Manager'), + ('vmware', 'VMware vCenter'), + ('satellite6', 'Red Hat Satellite 6'), + ('openstack', 'OpenStack'), + ('rhv', 'Red Hat Virtualization'), + ('controller', 'Red Hat Ansible Automation Platform'), + ('insights', 'Red Hat Insights'), + ('terraform', 'Terraform State'), + ('openshift_virtualization', 'OpenShift Virtualization'), + ], + default=None, + max_length=32, + ), + ), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 228cef7d6c95..2b96ed549f1f 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -933,6 +933,7 @@ class InventorySourceOptions(BaseModel): ('controller', _('Red Hat Ansible Automation Platform')), ('insights', _('Red Hat Insights')), ('terraform', _('Terraform State')), + ('openshift_virtualization', _('OpenShift Virtualization')), ] # From the options of the Django management base command @@ -1042,7 +1043,7 @@ def resolve_execution_environment(self): def cloud_credential_validation(source, cred): if not source: return None - if cred and source not in ('custom', 'scm'): + if cred and source not in ('custom', 'scm', 'openshift_virtualization'): # If a credential was provided, it's important that it matches # the actual inventory source being used (Amazon requires Amazon # credentials; Rackspace requires Rackspace credentials; etc...) @@ -1051,12 +1052,14 @@ def cloud_credential_validation(source, cred): # Allow an EC2 source to omit the credential. If Tower is running on # an EC2 instance with an IAM Role assigned, boto will use credentials # from the instance metadata instead of those explicitly provided. - elif source in CLOUD_PROVIDERS and source != 'ec2': + elif source in CLOUD_PROVIDERS and source not in ['ec2', 'openshift_virtualization']: return _('Credential is required for a cloud source.') elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'): return _('Credentials of type machine, source control, insights and vault are disallowed for custom inventory sources.') elif source == 'scm' and cred and cred.credential_type.kind in ('insights', 'vault'): return _('Credentials of type insights and vault are disallowed for scm inventory sources.') + elif source == 'openshift_virtualization' and cred and cred.credential_type.kind != 'kubernetes': + return _('Credentials of type kubernetes is requred for openshift_virtualization inventory sources.') return None def get_cloud_credential(self): @@ -1693,6 +1696,16 @@ class insights(PluginFileInjector): use_fqcn = True +class openshift_virtualization(PluginFileInjector): + plugin_name = 'kubevirt' + base_injector = 'template' + namespace = 'kubevirt' + collection = 'core' + downstream_namespace = 'redhat' + downstream_collection = 'openshift_virtualization' + use_fqcn = True + + class constructed(PluginFileInjector): plugin_name = 'constructed' namespace = 'ansible' diff --git a/awx/main/tests/data/inventory/plugins/openshift_virtualization/env.json b/awx/main/tests/data/inventory/plugins/openshift_virtualization/env.json new file mode 100644 index 000000000000..a44de77b88cb --- /dev/null +++ b/awx/main/tests/data/inventory/plugins/openshift_virtualization/env.json @@ -0,0 +1,5 @@ +{ + "K8S_AUTH_HOST": "https://foo.invalid", + "K8S_AUTH_API_KEY": "fooo", + "K8S_AUTH_VERIFY_SSL": "False" +} diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 80bc5429c1d2..0df9a132260d 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -46,6 +46,8 @@ def generate_fake_var(element): def credential_kind(source): """Given the inventory source kind, return expected credential kind""" + if source == 'openshift_virtualization': + return 'kubernetes_bearer_token' return source.replace('ec2', 'aws') diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d0fd7b115cad..93c6e6f60d1d 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -783,6 +783,11 @@ TERRAFORM_INSTANCE_ID_VAR = 'id' TERRAFORM_EXCLUDE_EMPTY_GROUPS = True +# ------------------------ +# OpenShift Virtualization +# ------------------------ +OPENSHIFT_VIRTUALIZATION_EXCLUDE_EMPTY_GROUPS = True + # --------------------- # ----- Custom ----- # --------------------- diff --git a/awx/ui/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.js b/awx/ui/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.js index bfc572a4e83d..3af847808f1e 100644 --- a/awx/ui/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.js +++ b/awx/ui/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.js @@ -56,6 +56,10 @@ describe('', () => { ['satellite6', 'Red Hat Satellite 6'], ['openstack', 'OpenStack'], ['rhv', 'Red Hat Virtualization'], + [ + 'openshift_virtualization', + 'Red Hat OpenShift Virtualization', + ], ['controller', 'Red Hat Ansible Automation Platform'], ], }, diff --git a/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js b/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js index 2be9d8b54d5d..a77dc52c9908 100644 --- a/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js +++ b/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js @@ -23,6 +23,8 @@ const ansibleDocUrls = { 'https://docs.ansible.com/ansible/latest/collections/ansible/builtin/constructed_inventory.html', terraform: 'https://github.com/ansible-collections/cloud.terraform/blob/main/docs/cloud.terraform.terraform_state_inventory.rst', + openshift_virtualization: + 'https://kubevirt.io/kubevirt.core/latest/plugins/kubevirt.html', }; const getInventoryHelpTextStrings = () => ({ @@ -121,7 +123,7 @@ const getInventoryHelpTextStrings = () => ({
{value && (
- {t`If you want the Inventory Source to update on launch , click on Update on Launch, + {t`If you want the Inventory Source to update on launch , click on Update on Launch, and also go to `} {value.name} {t`and click on Update Revision on Launch.`} @@ -140,7 +142,7 @@ const getInventoryHelpTextStrings = () => ({
{value && (
- {t`If you want the Inventory Source to update on launch , click on Update on Launch, + {t`If you want the Inventory Source to update on launch , click on Update on Launch, and also go to `} {value.name} {t`and click on Update Revision on Launch`} diff --git a/awx/ui/src/screens/Inventory/shared/InventorySourceForm.js b/awx/ui/src/screens/Inventory/shared/InventorySourceForm.js index 06172d961a17..88c5a5072a8a 100644 --- a/awx/ui/src/screens/Inventory/shared/InventorySourceForm.js +++ b/awx/ui/src/screens/Inventory/shared/InventorySourceForm.js @@ -26,6 +26,7 @@ import { TerraformSubForm, VMwareSubForm, VirtualizationSubForm, + OpenShiftVirtualizationSubForm, } from './InventorySourceSubForms'; const buildSourceChoiceOptions = (options) => { @@ -231,6 +232,15 @@ const InventorySourceFormFields = ({ sourceOptions={sourceOptions} /> ), + openshift_virtualization: ( + + ), }[sourceField.value] } diff --git a/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.js b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.js new file mode 100644 index 000000000000..4f725c2522e2 --- /dev/null +++ b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.js @@ -0,0 +1,64 @@ +import React, { useCallback } from 'react'; +import { useField, useFormikContext } from 'formik'; + +import { t } from '@lingui/macro'; +import { useConfig } from 'contexts/Config'; +import getDocsBaseUrl from 'util/getDocsBaseUrl'; +import CredentialLookup from 'components/Lookup/CredentialLookup'; +import { required } from 'util/validators'; +import { + OptionsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, + SourceVarsField, +} from './SharedFields'; +import getHelpText from '../Inventory.helptext'; + +const OpenShiftVirtualizationSubForm = ({ autoPopulateCredential }) => { + const helpText = getHelpText(); + const { setFieldValue, setFieldTouched } = useFormikContext(); + const [credentialField, credentialMeta, credentialHelpers] = + useField('credential'); + const config = useConfig(); + + const handleCredentialUpdate = useCallback( + (value) => { + setFieldValue('credential', value); + setFieldTouched('credential', true, false); + }, + [setFieldValue, setFieldTouched] + ); + + const docsBaseUrl = getDocsBaseUrl(config); + return ( + <> + credentialHelpers.setTouched()} + onChange={handleCredentialUpdate} + value={credentialField.value} + required + autoPopulate={autoPopulateCredential} + validate={required(t`Select a value for this field`)} + /> + + + + + + + + ); +}; + +export default OpenShiftVirtualizationSubForm; diff --git a/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.test.js b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.test.js new file mode 100644 index 000000000000..6b289600a98c --- /dev/null +++ b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/OpenShiftVirtualizationSubForm.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Formik } from 'formik'; +import { CredentialsAPI } from 'api'; +import { mountWithContexts } from '../../../../../testUtils/enzymeHelpers'; +import VirtualizationSubForm from './VirtualizationSubForm'; + +jest.mock('../../../../api'); + +const initialValues = { + credential: null, + overwrite: false, + overwrite_vars: false, + source_path: '', + source_project: null, + source_script: null, + source_vars: '---\n', + update_cache_timeout: 0, + update_on_launch: true, + verbosity: 1, +}; + +describe('', () => { + let wrapper; + + beforeEach(async () => { + CredentialsAPI.read.mockResolvedValue({ + data: { count: 0, results: [] }, + }); + + await act(async () => { + wrapper = mountWithContexts( + + + + ); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('should render subform fields', () => { + expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); + expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); + expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); + expect( + wrapper.find('FormGroup[label="Cache timeout (seconds)"]') + ).toHaveLength(1); + expect( + wrapper.find('VariablesField[label="Source variables"]') + ).toHaveLength(1); + }); + + test('should make expected api calls', () => { + expect(CredentialsAPI.read).toHaveBeenCalledTimes(1); + expect(CredentialsAPI.read).toHaveBeenCalledWith({ + credential_type__namespace: 'rhv', + order_by: 'name', + page: 1, + page_size: 5, + }); + }); +}); diff --git a/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/index.js b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/index.js index 27bcf5a31556..3cd87cb7e591 100644 --- a/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/index.js +++ b/awx/ui/src/screens/Inventory/shared/InventorySourceSubForms/index.js @@ -9,3 +9,4 @@ export { default as ControllerSubForm } from './ControllerSubForm'; export { default as TerraformSubForm } from './TerraformSubForm'; export { default as VMwareSubForm } from './VMwareSubForm'; export { default as VirtualizationSubForm } from './VirtualizationSubForm'; +export { default as OpenShiftVirtualizationSubForm } from './OpenShiftVirtualizationSubForm'; diff --git a/awx_collection/plugins/modules/inventory_source.py b/awx_collection/plugins/modules/inventory_source.py index 216ebce3d8aa..76f1d4234eb2 100644 --- a/awx_collection/plugins/modules/inventory_source.py +++ b/awx_collection/plugins/modules/inventory_source.py @@ -42,7 +42,8 @@ source: description: - The source to use for this group. - choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform" ] + choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform", + "openshift_virtualization" ] type: str source_path: description: @@ -170,7 +171,22 @@ def main(): # # How do we handle manual and file? The controller does not seem to be able to activate them # - source=dict(choices=["scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "controller", "insights", "terraform"]), + source=dict( + choices=[ + "scm", + "ec2", + "gce", + "azure_rm", + "vmware", + "satellite6", + "openstack", + "rhv", + "controller", + "insights", + "terraform", + "openshift_virtualization", + ] + ), source_path=dict(), source_vars=dict(type='dict'), enabled_var=dict(),