Skip to content

Commit 9458b0f

Browse files
authored
Set the installation mode for operator policies (stolostron#3462)
The policy wizard now has an installation mode that copies the OpenShift operator catalog. When in single namespace mode, the single "Installed Namespace" text box controls spec.subscription.namespace and spec.operatorGroup.targetNamespaces[0]. Relates: https://issues.redhat.com/browse/ACM-11078 Signed-off-by: mprahl <mprahl@users.noreply.github.com>
1 parent 3685060 commit 9458b0f

File tree

3 files changed

+293
-78
lines changed

3 files changed

+293
-78
lines changed

frontend/public/locales/en/translation.json

+4
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@
348348
"A saved search already exists for the current search criteria.": "A saved search already exists for the current search criteria.",
349349
"A saved search query is already using the name {{searchName}}. Please choose a different name.": "A saved search query is already using the name {{searchName}}. Please choose a different name.",
350350
"A security control category represent specific requirements for one or more standards. For example, a System and Information Integrity category might indicate that your policy contains a data transfer protocol to protect personal information, as required by the HIPAA and PCI standards.": "A security control category represent specific requirements for one or more standards. For example, a System and Information Integrity category might indicate that your policy contains a data transfer protocol to protect personal information, as required by the HIPAA and PCI standards.",
351+
"A specific namespace on the cluster": "A specific namespace on the cluster",
351352
"A vCenter server root CA certificate that, when added, reduces the number of web browser certificate warnings.": "A vCenter server root CA certificate that, when added, reduces the number of web browser certificate warnings.",
352353
"Accelerated Computing": "Accelerated Computing",
353354
"Access key ID": "Access key ID",
@@ -429,6 +430,7 @@
429430
"All clusters": "All clusters",
430431
"All copied": "All copied",
431432
"All DNS records must be subdomains of this base and include the cluster name. This cannot be changed after cluster installation.": "All DNS records must be subdomains of this base and include the cluster name. This cannot be changed after cluster installation.",
433+
"All namespaces on the cluster (default)": "All namespaces on the cluster (default)",
432434
"All resources in this group have deployed on the target clusters, although their status might not be successful.": "All resources in this group have deployed on the target clusters, although their status might not be successful.",
433435
"All Subscriptions": "All Subscriptions",
434436
"Allow applications to have empty resources": "Allow applications to have empty resources",
@@ -1613,6 +1615,8 @@
16131615
"Install Plan Approval": "Install Plan Approval",
16141616
"Install plan approved": "Install plan approved",
16151617
"Install the operator": "Install the operator",
1618+
"Installation Mode": "Installation Mode",
1619+
"Installed Namespace": "Installed Namespace",
16161620
"installing": "Installing",
16171621
"InstallPlanApproval determines if subscription installation plans are applied automatically.": "InstallPlanApproval determines if subscription installation plans are applied automatically.",
16181622
"Instance profile": "Instance profile",

frontend/src/wizards/Governance/Policy/PolicyWizard.tsx

+180-78
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { ExternalLinkAltIcon } from '@patternfly/react-icons'
1515
import get from 'get-value'
1616
import { klona } from 'klona/json'
17-
import { Fragment, ReactNode, useContext, useMemo } from 'react'
17+
import { Fragment, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
1818
import set from 'set-value'
1919
import {
2020
EditMode,
@@ -40,7 +40,9 @@ import {
4040
WizTextInput,
4141
ItemContext,
4242
useItem,
43+
useData,
4344
} from '@patternfly-labs/react-form-wizard'
45+
import { FormGroup as CoreFormGroup, Radio as CoreRadio } from '@patternfly/react-core'
4446
import { WizardPage } from '../../WizardPage'
4547
import { NavigationPath } from '../../../NavigationPath'
4648
import { IResource } from '../../common/resources/IResource'
@@ -261,6 +263,7 @@ export function PolicyWizardTemplates(props: { policies: IResource[] }) {
261263
const editMode = useEditMode()
262264
const selectorPath = 'objectDefinition.spec.namespaceSelector'
263265
const selectorMatchLabels = `${selectorPath}.matchLabels`
266+
264267
const { t } = useTranslation()
265268

266269
return (
@@ -384,83 +387,7 @@ export function PolicyWizardTemplates(props: { policies: IResource[] }) {
384387

385388
{/* OperatorPolicy */}
386389
<WizHidden hidden={(template: any) => template?.objectDefinition?.kind !== 'OperatorPolicy'}>
387-
<div>
388-
<Title headingLevel="h6">{t('Operator policy')}</Title>
389-
<Text component="small">{t('An Operator policy creates operators on managed clusters.')}</Text>
390-
</div>
391-
392-
<WizTextInput
393-
path="objectDefinition.metadata.name"
394-
label={t('Name')}
395-
required
396-
helperText={t('Name needs to be unique to the namespace on each of the managed clusters.')}
397-
validation={validateKubernetesResourceName}
398-
/>
399-
<Form>
400-
<FormFieldGroupExpandable
401-
isExpanded
402-
header={
403-
<FormFieldGroupHeader titleText={{ text: 'Operator Subscription', id: 'form-field-group-sub' }} />
404-
}
405-
>
406-
<WizTextInput
407-
path="objectDefinition.spec.subscription.name"
408-
label={t('Name')}
409-
labelHelp={t(
410-
'operatorPolicy.subscription.labelHelper',
411-
'This is the package name of the Operator to install, which might be different from the Display Name used in the catalog.'
412-
)}
413-
required
414-
validation={validateKubernetesResourceName}
415-
/>
416-
<WizTextInput
417-
path="objectDefinition.spec.subscription.namespace"
418-
label={t('Namespace')}
419-
labelHelp={t('The operator is installed in this namespace.')}
420-
/>
421-
<WizTextInput
422-
path="objectDefinition.spec.subscription.channel"
423-
label={t('Channel')}
424-
labelHelp={t('operatorPolicy.channel.labelHelper')}
425-
/>
426-
<WizRadioGroup
427-
path="objectDefinition.spec.subscription.installPlanApproval"
428-
label={t('Install Plan Approval')}
429-
>
430-
<Radio id="operator-policy-automatic" label={t('Automatic')} value="Automatic" />
431-
<Radio id="operator-policy-Manual" label={t('Manual')} value="Manual" />
432-
</WizRadioGroup>
433-
<WizTextInput
434-
path="objectDefinition.spec.subscription.source"
435-
label={t('Source')}
436-
labelHelp={t('operatorPolicy.source.labelHelper')}
437-
/>
438-
<WizTextInput
439-
path="objectDefinition.spec.subscription.sourceNamespace"
440-
label={t('Source Namespace')}
441-
labelHelp={t('operatorPolicy.sourceNamespace.labelHelper')}
442-
/>
443-
<WizTextInput
444-
path="objectDefinition.spec.subscription.startingCSV"
445-
label={t('Starting CSV')}
446-
placeholder={t('Enter the ClusterServiceVersion')}
447-
labelHelp={t(
448-
'operatorPolicy.startingCsv.labelHelper',
449-
`If you want to install a particular version of your Operator, specify the startingCSV property.`
450-
)}
451-
/>
452-
</FormFieldGroupExpandable>
453-
</Form>
454-
<WizStringsInput
455-
id="operator-policy-versions"
456-
path={`objectDefinition.spec.versions`}
457-
label={t('Allowed Cluster Service Versions')}
458-
placeholder={t('Add versions')}
459-
labelHelp={t(
460-
'operatorpolicy.version.labelHelper',
461-
`Versions is a list of non-empty strings that specify which installed versions are compliant when set to 'inform' mode, and which installPlans are approved when you set the parameter to 'enforce' mode.`
462-
)}
463-
/>
390+
<OperatorPolicy />
464391
</WizHidden>
465392

466393
{/* ConfigurationPolicy */}
@@ -620,6 +547,181 @@ export function isExistingTemplateName(name: string, policies: IResource[]) {
620547
return false
621548
}
622549

550+
function OperatorPolicy() {
551+
const { t } = useTranslation()
552+
const template: any = useItem()
553+
554+
const { update } = useData()
555+
556+
const [allNamespacesMode, setAllNamespacesMode] = useState<boolean>(
557+
!template?.objectDefinition?.spec?.operatorGroup?.targetNamespaces
558+
)
559+
560+
const setTargetNamespace = useCallback(
561+
(targetNamespace: string | undefined | null) => {
562+
// Handle when given a null value
563+
if (!targetNamespace) {
564+
if (template?.objectDefinition?.spec?.operatorGroup?.targetNamespaces) {
565+
// If the operator group only has targetNamespaces set, then remove the entire operatorGroup section from
566+
// the policy.
567+
if (Object.keys(template?.objectDefinition?.spec?.operatorGroup).length === 1) {
568+
delete template.objectDefinition.spec.operatorGroup
569+
} else {
570+
delete template.objectDefinition.spec.operatorGroup.targetNamespaces
571+
}
572+
573+
update()
574+
}
575+
576+
return
577+
}
578+
579+
if (allNamespacesMode) {
580+
return
581+
}
582+
583+
if (!template?.objectDefinition) {
584+
return
585+
}
586+
587+
if (!template.objectDefinition?.spec) {
588+
template.objectDefinition.spec = { operatorGroup: {} }
589+
}
590+
591+
if (!template.objectDefinition.spec?.operatorGroup) {
592+
template.objectDefinition.spec.operatorGroup = {}
593+
}
594+
595+
if (
596+
template.objectDefinition.spec.operatorGroup.targetNamespaces &&
597+
template.objectDefinition.spec.operatorGroup.targetNamespaces[0] === targetNamespace
598+
) {
599+
return
600+
}
601+
602+
template.objectDefinition.spec.operatorGroup.targetNamespaces = [targetNamespace]
603+
604+
update()
605+
},
606+
[allNamespacesMode, template, update]
607+
)
608+
609+
useEffect(() => {
610+
setTargetNamespace(template?.objectDefinition?.spec?.subscription?.namespace || '')
611+
}, [allNamespacesMode, template, setTargetNamespace])
612+
613+
return (
614+
<Fragment>
615+
<div>
616+
<Title headingLevel="h6">{t('Operator policy')}</Title>
617+
<Text component="small">{t('An Operator policy creates operators on managed clusters.')}</Text>
618+
</div>
619+
620+
<WizTextInput
621+
path="objectDefinition.metadata.name"
622+
label={t('Name')}
623+
required
624+
helperText={t('Name needs to be unique to the namespace on each of the managed clusters.')}
625+
validation={validateKubernetesResourceName}
626+
/>
627+
628+
<CoreFormGroup fieldId="operator-namespaces" label={t('Installation Mode')} isRequired={true}>
629+
<CoreRadio
630+
id="operator-all-namespaces"
631+
name="operator-namespaces"
632+
label={t('All namespaces on the cluster (default)')}
633+
checked={allNamespacesMode}
634+
onChange={(checked: boolean) => {
635+
setAllNamespacesMode(checked)
636+
637+
if (checked) {
638+
setTargetNamespace(null)
639+
}
640+
}}
641+
/>
642+
<CoreRadio
643+
id="operator-single-namespace"
644+
name="operator-namespaces"
645+
label={t('A specific namespace on the cluster')}
646+
checked={!allNamespacesMode}
647+
onChange={(checked: boolean) => {
648+
setAllNamespacesMode(!checked)
649+
}}
650+
/>
651+
</CoreFormGroup>
652+
653+
<WizTextInput
654+
path="objectDefinition.spec.subscription.namespace"
655+
label={t('Installed Namespace')}
656+
labelHelp={t('The operator is installed in this namespace.')}
657+
required={!allNamespacesMode}
658+
onValueChange={(value: any) => {
659+
setTargetNamespace(value)
660+
}}
661+
/>
662+
663+
<Form>
664+
<FormFieldGroupExpandable
665+
isExpanded
666+
header={<FormFieldGroupHeader titleText={{ text: 'Operator Subscription', id: 'form-field-group-sub' }} />}
667+
>
668+
<WizTextInput
669+
path="objectDefinition.spec.subscription.name"
670+
label={t('Name')}
671+
labelHelp={t(
672+
'operatorPolicy.subscription.labelHelper',
673+
'This is the package name of the Operator to install, which might be different from the Display Name used in the catalog.'
674+
)}
675+
required
676+
validation={validateKubernetesResourceName}
677+
/>
678+
<WizTextInput
679+
path="objectDefinition.spec.subscription.channel"
680+
label={t('Channel')}
681+
labelHelp={t('operatorPolicy.channel.labelHelper')}
682+
/>
683+
<WizRadioGroup
684+
path="objectDefinition.spec.subscription.installPlanApproval"
685+
label={t('Install Plan Approval')}
686+
>
687+
<Radio id="operator-policy-automatic" label={t('Automatic')} value="Automatic" />
688+
<Radio id="operator-policy-Manual" label={t('Manual')} value="Manual" />
689+
</WizRadioGroup>
690+
<WizTextInput
691+
path="objectDefinition.spec.subscription.source"
692+
label={t('Source')}
693+
labelHelp={t('operatorPolicy.source.labelHelper')}
694+
/>
695+
<WizTextInput
696+
path="objectDefinition.spec.subscription.sourceNamespace"
697+
label={t('Source Namespace')}
698+
labelHelp={t('operatorPolicy.sourceNamespace.labelHelper')}
699+
/>
700+
<WizTextInput
701+
path="objectDefinition.spec.subscription.startingCSV"
702+
label={t('Starting CSV')}
703+
placeholder={t('Enter the ClusterServiceVersion')}
704+
labelHelp={t(
705+
'operatorPolicy.startingCsv.labelHelper',
706+
`If you want to install a particular version of your Operator, specify the startingCSV property.`
707+
)}
708+
/>
709+
</FormFieldGroupExpandable>
710+
</Form>
711+
<WizStringsInput
712+
id="operator-policy-versions"
713+
path={`objectDefinition.spec.versions`}
714+
label={t('Allowed Cluster Service Versions')}
715+
placeholder={t('Add versions')}
716+
labelHelp={t(
717+
'operatorpolicy.version.labelHelper',
718+
`Versions is a list of non-empty strings that specify which installed versions are compliant when set to 'inform' mode, and which installPlans are approved when you set the parameter to 'enforce' mode.`
719+
)}
720+
/>
721+
</Fragment>
722+
)
723+
}
724+
623725
function ObjectTemplate() {
624726
const template: any = useItem()
625727
const { t } = useTranslation()

0 commit comments

Comments
 (0)