Add OpenShift Virtualization Inventory source option (#15047)

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
This commit is contained in:
Chad Ferman
2024-06-14 12:38:37 -05:00
committed by GitHub
parent d94f766fcb
commit 31a086b11a
13 changed files with 255 additions and 7 deletions

View File

@@ -14,7 +14,7 @@ __all__ = [
'STANDARD_INVENTORY_UPDATE_ENV', '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 = [ PRIVILEGE_ESCALATION_METHODS = [
('sudo', _('Sudo')), ('sudo', _('Sudo')),
('su', _('Su')), ('su', _('Su')),

View File

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

View File

@@ -933,6 +933,7 @@ class InventorySourceOptions(BaseModel):
('controller', _('Red Hat Ansible Automation Platform')), ('controller', _('Red Hat Ansible Automation Platform')),
('insights', _('Red Hat Insights')), ('insights', _('Red Hat Insights')),
('terraform', _('Terraform State')), ('terraform', _('Terraform State')),
('openshift_virtualization', _('OpenShift Virtualization')),
] ]
# From the options of the Django management base command # From the options of the Django management base command
@@ -1042,7 +1043,7 @@ class InventorySourceOptions(BaseModel):
def cloud_credential_validation(source, cred): def cloud_credential_validation(source, cred):
if not source: if not source:
return None 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 # If a credential was provided, it's important that it matches
# the actual inventory source being used (Amazon requires Amazon # the actual inventory source being used (Amazon requires Amazon
# credentials; Rackspace requires Rackspace credentials; etc...) # credentials; Rackspace requires Rackspace credentials; etc...)
@@ -1051,12 +1052,14 @@ class InventorySourceOptions(BaseModel):
# Allow an EC2 source to omit the credential. If Tower is running on # 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 # an EC2 instance with an IAM Role assigned, boto will use credentials
# from the instance metadata instead of those explicitly provided. # 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.') return _('Credential is required for a cloud source.')
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'): 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.') 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'): 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.') 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 return None
def get_cloud_credential(self): def get_cloud_credential(self):
@@ -1693,6 +1696,16 @@ class insights(PluginFileInjector):
use_fqcn = True 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): class constructed(PluginFileInjector):
plugin_name = 'constructed' plugin_name = 'constructed'
namespace = 'ansible' namespace = 'ansible'

View File

@@ -0,0 +1,5 @@
{
"K8S_AUTH_HOST": "https://foo.invalid",
"K8S_AUTH_API_KEY": "fooo",
"K8S_AUTH_VERIFY_SSL": "False"
}

View File

@@ -46,6 +46,8 @@ def generate_fake_var(element):
def credential_kind(source): def credential_kind(source):
"""Given the inventory source kind, return expected credential kind""" """Given the inventory source kind, return expected credential kind"""
if source == 'openshift_virtualization':
return 'kubernetes_bearer_token'
return source.replace('ec2', 'aws') return source.replace('ec2', 'aws')

View File

@@ -783,6 +783,11 @@ INSIGHTS_EXCLUDE_EMPTY_GROUPS = False
TERRAFORM_INSTANCE_ID_VAR = 'id' TERRAFORM_INSTANCE_ID_VAR = 'id'
TERRAFORM_EXCLUDE_EMPTY_GROUPS = True TERRAFORM_EXCLUDE_EMPTY_GROUPS = True
# ------------------------
# OpenShift Virtualization
# ------------------------
OPENSHIFT_VIRTUALIZATION_EXCLUDE_EMPTY_GROUPS = True
# --------------------- # ---------------------
# ----- Custom ----- # ----- Custom -----
# --------------------- # ---------------------

View File

@@ -56,6 +56,10 @@ describe('<InventorySourceAdd />', () => {
['satellite6', 'Red Hat Satellite 6'], ['satellite6', 'Red Hat Satellite 6'],
['openstack', 'OpenStack'], ['openstack', 'OpenStack'],
['rhv', 'Red Hat Virtualization'], ['rhv', 'Red Hat Virtualization'],
[
'openshift_virtualization',
'Red Hat OpenShift Virtualization',
],
['controller', 'Red Hat Ansible Automation Platform'], ['controller', 'Red Hat Ansible Automation Platform'],
], ],
}, },

View File

@@ -23,6 +23,8 @@ const ansibleDocUrls = {
'https://docs.ansible.com/ansible/latest/collections/ansible/builtin/constructed_inventory.html', 'https://docs.ansible.com/ansible/latest/collections/ansible/builtin/constructed_inventory.html',
terraform: terraform:
'https://github.com/ansible-collections/cloud.terraform/blob/main/docs/cloud.terraform.terraform_state_inventory.rst', '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 = () => ({ const getInventoryHelpTextStrings = () => ({

View File

@@ -26,6 +26,7 @@ import {
TerraformSubForm, TerraformSubForm,
VMwareSubForm, VMwareSubForm,
VirtualizationSubForm, VirtualizationSubForm,
OpenShiftVirtualizationSubForm,
} from './InventorySourceSubForms'; } from './InventorySourceSubForms';
const buildSourceChoiceOptions = (options) => { const buildSourceChoiceOptions = (options) => {
@@ -231,6 +232,15 @@ const InventorySourceFormFields = ({
sourceOptions={sourceOptions} sourceOptions={sourceOptions}
/> />
), ),
openshift_virtualization: (
<OpenShiftVirtualizationSubForm
autoPopulateCredential={
!source?.id ||
source?.source !== 'openshift_virtualization'
}
sourceOptions={sourceOptions}
/>
),
}[sourceField.value] }[sourceField.value]
} }
</FormColumnLayout> </FormColumnLayout>

View File

@@ -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 (
<>
<CredentialLookup
credentialTypeNamespace="kubernetes_bearer_token"
label={t`Credential`}
helperTextInvalid={credentialMeta.error}
isValid={!credentialMeta.touched || !credentialMeta.error}
onBlur={() => credentialHelpers.setTouched()}
onChange={handleCredentialUpdate}
value={credentialField.value}
required
autoPopulate={autoPopulateCredential}
validate={required(t`Select a value for this field`)}
/>
<VerbosityField />
<HostFilterField />
<EnabledVarField />
<EnabledValueField />
<OptionsField />
<SourceVarsField
popoverContent={helpText.sourceVars(
docsBaseUrl,
'openshift_virtualization'
)}
/>
</>
);
};
export default OpenShiftVirtualizationSubForm;

View File

@@ -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('<VirtualizationSubForm />', () => {
let wrapper;
beforeEach(async () => {
CredentialsAPI.read.mockResolvedValue({
data: { count: 0, results: [] },
});
await act(async () => {
wrapper = mountWithContexts(
<Formik initialValues={initialValues}>
<VirtualizationSubForm />
</Formik>
);
});
});
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,
});
});
});

View File

@@ -9,3 +9,4 @@ export { default as ControllerSubForm } from './ControllerSubForm';
export { default as TerraformSubForm } from './TerraformSubForm'; export { default as TerraformSubForm } from './TerraformSubForm';
export { default as VMwareSubForm } from './VMwareSubForm'; export { default as VMwareSubForm } from './VMwareSubForm';
export { default as VirtualizationSubForm } from './VirtualizationSubForm'; export { default as VirtualizationSubForm } from './VirtualizationSubForm';
export { default as OpenShiftVirtualizationSubForm } from './OpenShiftVirtualizationSubForm';

View File

@@ -42,7 +42,8 @@ options:
source: source:
description: description:
- The source to use for this group. - 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 type: str
source_path: source_path:
description: 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 # 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_path=dict(),
source_vars=dict(type='dict'), source_vars=dict(type='dict'),
enabled_var=dict(), enabled_var=dict(),