Skip to content

Commit 2bcfe01

Browse files
Add ACM Submariner pre-pair validation
Watch ManagedClusterAddOn health for the selected cluster pair, gate the Configure step on canProceed, and keep evaluation in mco utils/hooks. How Submariner is checked (ACM path): - Hub watch only: ManagedClusterAddOn named "submariner" per managed cluster namespace (RHACM Networking docs). No ManagedClusterView in this PR; spoke Broker/Cluster CRs are for upstream/Globalnet later. - NotInstalled: addon object missing (usual) or watch 404 (absent API resource) — not a condition on the MCA spec. Both missing => skip validation and allow Next (UX: Cluster network - Skipped). - Per cluster after addon exists: Available first (SubmarinerAgentAvailable=True); then connected (SubmarinerConnectionDegraded=False) => Healthy. ConnectionDegraded, AgentDegraded, RouteAgentConnectionDegraded, or unlabeled gateway => Degraded. Available not ready / connection unset => Progressing. - Pair gate: both NotInstalled or both Healthy => canProceed; one-sided install => Inconsistent (block); otherwise Degraded/Progressing block. Why this combination (vs connectivity-only): - Available then ConnectionDegraded matches ACM's documented MCA conditions for deploy + established connections. Extra degraded signals (agent/route/gateway) fail closed so we do not treat a half-ready addon as connected. Can narrow the gate later if product wants ConnectionDegraded alone. Out of scope here: Globalnet/CIDR overlap warnings, MCV lifecycle.
1 parent 12e8396 commit 2bcfe01

15 files changed

Lines changed: 457 additions & 14 deletions

locales/en/plugin__odf-console.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@
506506
"Check": "Check",
507507
"Check potential reasons": "Check potential reasons",
508508
"check unsuccessful on the {{clusterName}}:": "check unsuccessful on the {{clusterName}}:",
509+
"Checking cluster network connectivity...": "Checking cluster network connectivity...",
509510
"Checking dependencies...": "Checking dependencies...",
510511
"Checking Local Storage Operator installation": "Checking Local Storage Operator installation",
511512
"Checks cannot be performed for the {{clusterName}}:": "Checks cannot be performed for the {{clusterName}}:",
@@ -554,7 +555,10 @@
554555
"Cluster name (ID)": "Cluster name (ID)",
555556
"cluster name search": "cluster name search",
556557
"cluster name search button": "cluster name search button",
558+
"Cluster network - Skipped": "Cluster network - Skipped",
557559
"Cluster network CIDR": "Cluster network CIDR",
560+
"Cluster network configuration in progress": "Cluster network configuration in progress",
561+
"Cluster network connected": "Cluster network connected",
558562
"Cluster Network Interface": "Cluster Network Interface",
559563
"Cluster operator": "Cluster operator",
560564
"Cluster Status": "Cluster Status",
@@ -794,6 +798,7 @@
794798
"Define where to sync or replicate your application volumes and Kubernetes object using a disaster recovery policy.": "Define where to sync or replicate your application volumes and Kubernetes object using a disaster recovery policy.",
795799
"Defines the object-store service and the bucket provisioner.": "Defines the object-store service and the bucket provisioner.",
796800
"Degraded": "Degraded",
801+
"Degraded - Cluster unhealthy": "Degraded - Cluster unhealthy",
797802
"delete": "delete",
798803
"Delete": "Delete",
799804
"Delete (expire current versions) {{ days }} days after creation.": "Delete (expire current versions) {{ days }} days after creation.",
@@ -854,6 +859,7 @@
854859
"Deploying the application on the target cluster.": "Deploying the application on the target cluster.",
855860
"Deployment": "Deployment",
856861
"Deployment details": "Deployment details",
862+
"Deployment is not using submariner. Selected cluster pairs lack submariner addon": "Deployment is not using submariner. Selected cluster pairs lack submariner addon",
857863
"Deployment is unavailable": "Deployment is unavailable",
858864
"Deployment Name": "Deployment Name",
859865
"Deployment type": "Deployment type",
@@ -2189,6 +2195,8 @@
21892195
"StorageConsumer Name": "StorageConsumer Name",
21902196
"StorageConsumers": "StorageConsumers",
21912197
"Stretch Cluster": "Stretch Cluster",
2198+
"Submariner": "Submariner",
2199+
"Submariner is not installed on one or both selected cluster pairs": "Submariner is not installed on one or both selected cluster pairs",
21922200
"Subpath": "Subpath",
21932201
"Subscription": "Subscription",
21942202
"Subscription deprecated": "Subscription deprecated",

packages/mco/components/create-dr-policy/CreateDRPolicyForm.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
2-
import { getMajorVersion } from '@odf/mco/utils';
2+
import { usePrePairNetworkValidation } from '@odf/mco/hooks';
3+
import { getMajorVersion, shouldRunPrePairValidation } from '@odf/mco/utils';
34
import {
45
ACM_DEFAULT_DOC_VERSION,
56
DRClusterModel,
@@ -12,10 +13,11 @@ import { getName } from '@odf/shared/selectors';
1213
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
1314
import { referenceForModel } from '@odf/shared/utils';
1415
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
15-
import { Wizard, WizardStep } from '@patternfly/react-core';
16+
import { FormGroup, Wizard, WizardStep } from '@patternfly/react-core';
1617
import {
1718
ACM_OPERATOR_SPEC_NAME,
1819
acmDocHome,
20+
acmSubmarinerDoc,
1921
BackendType,
2022
CreateDRPolicyStepNames,
2123
CreateDRPolicyWizardSteps,
@@ -31,6 +33,7 @@ import { ConfigureClusterPairStep } from './configure-cluster-pair-step';
3133
import './create-dr-policy.scss';
3234
import { CreateDRPolicyWizardFooter } from './footer';
3335
import { PolicyStep } from './policy-step';
36+
import { PrePairNetworkValidation } from './pre-pair-network-validation';
3437
import { ReviewDRPolicyStep } from './review-dr-policy-step';
3538
import { createPolicyPromises } from './utils/k8s-utils';
3639
import {
@@ -133,6 +136,7 @@ export const CreateDRPolicyForm: React.FC<CreateDRPolicyFormProps> = ({
133136
const [s3ErrorMessage, setS3ErrorMessage] = React.useState('');
134137
const [createErrorMessage, setCreateErrorMessage] = React.useState('');
135138
const [isLoading, setIsLoading] = React.useState(false);
139+
const [step, setStep] = React.useState(CreateDRPolicyWizardSteps.Clusters);
136140

137141
const [mirrorPeers, mirrorPeerLoaded, mirrorPeerLoadError] =
138142
useK8sWatchResource<MirrorPeerKind[]>({
@@ -253,13 +257,28 @@ export const CreateDRPolicyForm: React.FC<CreateDRPolicyFormProps> = ({
253257
const loadedError = mirrorPeerLoadError || drClustersLoadError;
254258

255259
const clusterNames = state.selectedClusters.map(getName);
260+
const shouldRunValidation = shouldRunPrePairValidation(
261+
state.selectedClusters.length,
262+
state.isClusterSelectionValid,
263+
state.replicationBackend === BackendType.DataFoundation
264+
);
265+
// Cluster pair validation runs only after the user leaves the clusters page.
266+
const validationActive =
267+
shouldRunValidation && step !== CreateDRPolicyWizardSteps.Clusters;
268+
const prePairValidation = usePrePairNetworkValidation(
269+
clusterNames,
270+
validationActive
271+
);
272+
const prePairValidationPassed =
273+
!validationActive || prePairValidation.canProceed;
256274

257275
const acmDocVersion = useDocVersion({
258276
defaultDocVersion: ACM_DEFAULT_DOC_VERSION,
259277
specName: ACM_OPERATOR_SPEC_NAME,
260278
});
261279

262280
const acmDoc = acmDocHome(acmDocVersion);
281+
const submarinerDoc = acmSubmarinerDoc(acmDocVersion);
263282

264283
const allDRClustersExist = selectedDRClusters.length === MAX_ALLOWED_CLUSTERS;
265284

@@ -273,22 +292,21 @@ export const CreateDRPolicyForm: React.FC<CreateDRPolicyFormProps> = ({
273292
state,
274293
allDRClustersExist
275294
),
276-
[CreateDRPolicyWizardSteps.Configure]: validateClusterInputs(
277-
state,
278-
allDRClustersExist
279-
),
295+
[CreateDRPolicyWizardSteps.Configure]: prePairValidation.canProceed,
280296
[CreateDRPolicyWizardSteps.Policy]: validatePolicyInputs(state),
281-
[CreateDRPolicyWizardSteps.Review]: validateDRPolicyInputs(
282-
state,
283-
allDRClustersExist
284-
),
297+
[CreateDRPolicyWizardSteps.Review]:
298+
validateDRPolicyInputs(state, allDRClustersExist) &&
299+
prePairValidationPassed,
285300
};
286301

287302
return (
288303
<Wizard
289304
className="mco-create-data-policy__wizard--height"
290305
navAriaLabel={t('Create DRPolicy steps')}
291306
isVisitRequired
307+
onStepChange={(_event, currentStep) =>
308+
setStep(currentStep.id as CreateDRPolicyWizardSteps)
309+
}
292310
footer={
293311
<CreateDRPolicyWizardFooter
294312
stepValidity={stepValidity}
@@ -320,7 +338,11 @@ export const CreateDRPolicyForm: React.FC<CreateDRPolicyFormProps> = ({
320338
name={stepNames[CreateDRPolicyWizardSteps.Configure]}
321339
isHidden={state.replicationBackend !== BackendType.DataFoundation}
322340
>
323-
<ConfigureClusterPairStep />
341+
<ConfigureClusterPairStep
342+
clusterNames={clusterNames}
343+
validation={prePairValidation}
344+
docHref={submarinerDoc}
345+
/>
324346
</WizardStep>
325347
<WizardStep
326348
id={CreateDRPolicyWizardSteps.Policy}
@@ -332,7 +354,20 @@ export const CreateDRPolicyForm: React.FC<CreateDRPolicyFormProps> = ({
332354
id={CreateDRPolicyWizardSteps.Review}
333355
name={stepNames[CreateDRPolicyWizardSteps.Review]}
334356
>
335-
<ReviewDRPolicyStep state={state} />
357+
<ReviewDRPolicyStep
358+
state={state}
359+
networkStatus={
360+
shouldRunValidation ? (
361+
<FormGroup fieldId="review-cluster-network">
362+
<PrePairNetworkValidation
363+
clusterNames={clusterNames}
364+
validation={prePairValidation}
365+
docHref={submarinerDoc}
366+
/>
367+
</FormGroup>
368+
) : undefined
369+
}
370+
/>
336371
</WizardStep>
337372
</Wizard>
338373
);

packages/mco/components/create-dr-policy/configure-cluster-pair-step.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import * as React from 'react';
2+
import { PrePairNetworkValidationState } from '@odf/mco/hooks';
23
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
34
import { Content, ContentVariants, Title } from '@patternfly/react-core';
5+
import { PrePairNetworkValidation } from './pre-pair-network-validation';
46

5-
export const ConfigureClusterPairStep: React.FC = () => {
7+
type ConfigureClusterPairStepProps = {
8+
clusterNames: string[];
9+
validation: PrePairNetworkValidationState;
10+
docHref?: string;
11+
};
12+
13+
export const ConfigureClusterPairStep: React.FC<
14+
ConfigureClusterPairStepProps
15+
> = ({ clusterNames, validation, docHref }) => {
616
const { t } = useCustomTranslation();
717

818
return (
@@ -17,6 +27,11 @@ export const ConfigureClusterPairStep: React.FC = () => {
1727
)}
1828
</Content>
1929
</Content>
30+
<PrePairNetworkValidation
31+
clusterNames={clusterNames}
32+
validation={validation}
33+
docHref={docHref}
34+
/>
2035
</div>
2136
);
2237
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from 'react';
2+
import { SubmarinerStatus } from '@odf/mco/constants';
3+
import { PrePairNetworkValidationState } from '@odf/mco/hooks';
4+
import { StatusBox } from '@odf/shared/generic/status-box';
5+
import {
6+
GreenCheckCircleIcon,
7+
RedExclamationCircleIcon,
8+
YellowExclamationTriangleIcon,
9+
} from '@odf/shared/status';
10+
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
11+
import { ViewDocumentation } from '@odf/shared/utils';
12+
import { StatusIconAndText } from '@openshift-console/dynamic-plugin-sdk';
13+
import { TFunction } from 'react-i18next';
14+
import {
15+
Content,
16+
ContentVariants,
17+
Spinner,
18+
Title,
19+
} from '@patternfly/react-core';
20+
21+
type StatusLine = {
22+
icon: React.ReactElement;
23+
title: string;
24+
description?: string;
25+
showDocLink?: boolean;
26+
};
27+
28+
const getSubmarinerStatusLine = (
29+
status: SubmarinerStatus,
30+
t: TFunction
31+
): StatusLine => {
32+
switch (status) {
33+
case SubmarinerStatus.Checking:
34+
return {
35+
icon: <Spinner size="sm" />,
36+
title: t('Checking cluster network connectivity...'),
37+
};
38+
case SubmarinerStatus.Progressing:
39+
return {
40+
icon: <Spinner size="sm" />,
41+
title: t('Cluster network configuration in progress'),
42+
};
43+
case SubmarinerStatus.Healthy:
44+
return {
45+
icon: <GreenCheckCircleIcon />,
46+
title: t('Cluster network connected'),
47+
};
48+
case SubmarinerStatus.NotInstalled:
49+
return {
50+
icon: <YellowExclamationTriangleIcon />,
51+
title: t('Cluster network - Skipped'),
52+
description: t(
53+
'Deployment is not using submariner. Selected cluster pairs lack submariner addon'
54+
),
55+
};
56+
case SubmarinerStatus.Inconsistent:
57+
return {
58+
icon: <RedExclamationCircleIcon />,
59+
title: t('Degraded - Cluster unhealthy'),
60+
description: t(
61+
'Submariner is not installed on one or both selected cluster pairs'
62+
),
63+
showDocLink: true,
64+
};
65+
case SubmarinerStatus.Degraded:
66+
default:
67+
return {
68+
icon: <RedExclamationCircleIcon />,
69+
title: t('Degraded - Cluster unhealthy'),
70+
showDocLink: true,
71+
};
72+
}
73+
};
74+
75+
export const PrePairNetworkValidation: React.FC<{
76+
clusterNames: string[];
77+
validation: PrePairNetworkValidationState;
78+
docHref?: string;
79+
}> = ({ clusterNames, validation, docHref }) => {
80+
const { t } = useCustomTranslation();
81+
const statusLine = getSubmarinerStatusLine(validation.status, t);
82+
83+
return (
84+
<StatusBox
85+
data={clusterNames}
86+
loaded={validation.loaded}
87+
loadError={validation.loadError}
88+
skeleton={
89+
<StatusIconAndText
90+
icon={<Spinner size="sm" />}
91+
title={t('Checking cluster network connectivity...')}
92+
/>
93+
}
94+
>
95+
<Title headingLevel="h3" size="md" className="pf-v6-u-mb-sm">
96+
{t('Submariner')}
97+
</Title>
98+
<StatusIconAndText icon={statusLine.icon} title={statusLine.title} />
99+
{statusLine.description && (
100+
<Content component={ContentVariants.small}>
101+
{statusLine.description}
102+
</Content>
103+
)}
104+
{statusLine.showDocLink && docHref && (
105+
<ViewDocumentation doclink={docHref} />
106+
)}
107+
</StatusBox>
108+
);
109+
};

packages/mco/components/create-dr-policy/review-dr-policy-step.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import { DRPolicyState } from './utils/reducer';
1818

1919
type ReviewDRPolicyStepProps = {
2020
state: DRPolicyState;
21+
networkStatus?: React.ReactNode;
2122
};
2223

2324
export const ReviewDRPolicyStep: React.FC<ReviewDRPolicyStepProps> = ({
2425
state,
26+
networkStatus,
2527
}) => {
2628
const { t } = useCustomTranslation();
2729
const clusterNames = state.selectedClusters.map(getName).join(', ');
@@ -57,6 +59,7 @@ export const ReviewDRPolicyStep: React.FC<ReviewDRPolicyStepProps> = ({
5759
{clusterNames}
5860
</ReviewAndCreationItem>
5961
</ReviewAndCreationGroup>
62+
{networkStatus}
6063
<ReviewAndCreationGroup title={t('Replication')}>
6164
<ReviewAndCreationItem label={t('Replication type:')}>
6265
{replicationLabel}

packages/mco/constants/doc.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const acmDocHome = (acmDocVersion) =>
88
`https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_management_for_kubernetes/${acmDocVersion}`;
99
export const acmDocBasePath = (acmDocVersion) =>
1010
`${acmDocHome(acmDocVersion)}/html-single`;
11+
export const acmSubmarinerDoc = (acmDocVersion) =>
12+
`${acmDocBasePath(acmDocVersion)}/networking/index#submariner`;
1113

1214
export const docLinks = (mcoDocVersion, acmDocVersion) => ({
1315
APPLY_POLICY: odfDRDocApplyPolicy(mcoDocVersion),

packages/mco/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './acm';
55
export * from './doc';
66
export * from './dashboard';
77
export * from './url-paths';
8+
export * from './submariner';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const SUBMARINER_ADDON_NAME = 'submariner';
2+
3+
export const SUBMARINER_ADDON_KIND =
4+
'addon.open-cluster-management.io~v1alpha1~ManagedClusterAddOn';
5+
6+
export const SUBMARINER_CONDITION_TYPES = {
7+
AVAILABLE: 'SubmarinerAgentAvailable', /* also SubMariner* casing */
8+
CONNECTION_DEGRADED: 'SubmarinerConnectionDegraded',
9+
ROUTE_AGENT_CONNECTION_DEGRADED: 'RouteAgentConnectionDegraded',
10+
AGENT_DEGRADED: 'SubmarinerAgentDegraded',
11+
GATEWAY_NODES_LABELED: 'SubmarinerGatewayNodesLabeled',
12+
};
13+
14+
export enum SubmarinerStatus {
15+
Checking = 'checking',
16+
NotInstalled = 'notInstalled',
17+
Progressing = 'progressing',
18+
Healthy = 'healthy',
19+
Degraded = 'degraded',
20+
Inconsistent = 'inconsistent',
21+
}

packages/mco/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './useProtectedAppsByCluster';
88
export * from './useDRPoliciesByClusterPair';
99
export * from './useActiveDROperations';
1010
export * from './use-ramen-config';
11+
export * from './use-pre-pair-network-validation';

0 commit comments

Comments
 (0)