Skip to content

Commit 2d9763a

Browse files
committed
feature(crr): add management-network endpoint & source-DNS Apply Actions rows and refine the certificate step
1 parent 548e3c7 commit 2d9763a

6 files changed

Lines changed: 157 additions & 32 deletions

File tree

src/react/locations/CRRSetupWizard/hooks/useImportDestinationCertificateMutation.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ describe('isCertificateAlreadyImported', () => {
1919
it('is true when the exact certificate is already in the truststore', () => {
2020
expect(isCertificateAlreadyImported(withCerts(['some-other-cert', CERT]), CERT)).toBe(true);
2121
});
22+
23+
it('ignores trailing-whitespace / line-ending differences so a re-import stays a no-op', () => {
24+
expect(isCertificateAlreadyImported(withCerts([CERT]), `${CERT}\n`)).toBe(true);
25+
expect(isCertificateAlreadyImported(withCerts([`${CERT}\n`]), CERT)).toBe(true);
26+
expect(isCertificateAlreadyImported(withCerts([CERT]), CERT.replace(/\n/g, '\r\n'))).toBe(true);
27+
});
2228
});

src/react/locations/CRRSetupWizard/hooks/useImportDestinationCertificateMutation.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import { useAddCertificateToZenkoConfigurationMutation } from '../../../../js/mu
33
import { getZenkoCRQuery } from '../../../queries';
44
import type { ZenkoCR } from '../../../truststore/Truststore';
55

6+
export const normalizeCertificate = (certificate: string): string => certificate.replace(/\r\n/g, '\n').trim();
7+
68
/** True when the PEM is already in the source truststore, so importing it again is a no-op. */
79
export const isCertificateAlreadyImported = (zenkoCR: ZenkoCR | undefined, certificate: string): boolean =>
8-
(zenkoCR?.spec?.egress?.extraCACerts ?? []).some((bundle) => bundle['ca.crt'] === certificate);
10+
(zenkoCR?.spec?.egress?.extraCACerts ?? []).some(
11+
(bundle) => normalizeCertificate(bundle['ca.crt']) === normalizeCertificate(certificate),
12+
);
913

1014
/**
1115
* Imports the destination CA certificate into the source cluster's truststore
@@ -22,12 +26,13 @@ export const useImportDestinationCertificateMutation = () => {
2226

2327
return useMutation({
2428
mutationFn: async ({ certificate }: { certificate: string }) => {
29+
const normalized = normalizeCertificate(certificate);
2530
// Read the latest cached CR at execution time, not the render-time snapshot.
2631
const currentCR = queryClient.getQueryData<ZenkoCR>(['zenkoCR']) ?? zenkoCR;
27-
if (isCertificateAlreadyImported(currentCR, certificate)) {
32+
if (isCertificateAlreadyImported(currentCR, normalized)) {
2833
return;
2934
}
30-
await addCertificate.mutateAsync({ certificate });
35+
await addCertificate.mutateAsync({ certificate: normalized });
3136
},
3237
});
3338
};

src/react/locations/CRRSetupWizard/steps/ApplyActionsStep/ApplyActionsStep.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ describe('ApplyActionsStep', () => {
5454
expect(screen.getByText('Configure paris-prod for Cross-Region Replication')).toBeInTheDocument();
5555

5656
const actions = [
57-
'Import Destination Certificate',
5857
'Create Account on Source: crr-src',
5958
'Create Bucket on Source: crr-src-bucket',
6059
'Create Account on Destination: crr-dest',
@@ -64,21 +63,24 @@ describe('ApplyActionsStep', () => {
6463
'Create IAM Role',
6564
'Attach Policy to Role',
6665
'Create Target Bucket: crr-target-bucket',
66+
'Register Destination S3 Endpoint',
67+
'Configure Source DNS Resolution',
68+
'Import Certificate into Truststore',
6769
'Create Location',
6870
'Create Replication Rule',
6971
];
7072
for (const action of actions) {
7173
expect(screen.getByText(action)).toBeInTheDocument();
7274
}
73-
expect(screen.getAllByText('Pending...').length).toBe(12);
75+
expect(screen.getAllByText('Pending...').length).toBe(14);
7476
// The setup only becomes confirmable once every action has succeeded.
7577
expect(screen.getByRole('button', { name: /Continue/i })).toBeDisabled();
7678
});
7779

7880
it('does not surface a source-account action when the user reuses an existing account', () => {
7981
render(<ApplyActionsStep {...VALUES} accountNameType="existing" />, { wrapper: Wrapper });
8082
expect(screen.queryByText(/Create Account on Source/i)).not.toBeInTheDocument();
81-
expect(screen.getAllByText('Pending...').length).toBe(11);
83+
expect(screen.getAllByText('Pending...').length).toBe(13);
8284
});
8385

8486
it('does not surface the source bucket, target bucket or replication rule when no rule is requested', () => {
@@ -88,7 +90,7 @@ describe('ApplyActionsStep', () => {
8890
expect(screen.queryByText(/Create Bucket on Source/i)).not.toBeInTheDocument();
8991
expect(screen.queryByText(/Create Target Bucket/i)).not.toBeInTheDocument();
9092
expect(screen.queryByText(/Create Replication Rule/i)).not.toBeInTheDocument();
91-
expect(screen.getAllByText('Pending...').length).toBe(9);
93+
expect(screen.getAllByText('Pending...').length).toBe(11);
9294
});
9395

9496
it('falls back to "ARTESCA" in the title when Verify returned no instance name', () => {

src/react/locations/CRRSetupWizard/steps/ApplyActionsStep/ApplyActionsStep.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Form, Icon, Stack, Text } from '@scality/core-ui';
1+
import { Form, Icon, Loader, Stack, Text } from '@scality/core-ui';
22
import { useStepper } from '@scality/core-ui/dist/components/steppers/Stepper.component';
33
import { Button } from '@scality/core-ui/dist/next';
44
import { useCreateBucket, useSetBucketReplication, useSetBucketVersioning } from '@scality/data-browser-library';
@@ -61,6 +61,14 @@ const StatusCell = ({ view, onRetry }: { view: StepView; onRetry: () => void })
6161
</StatusBox>
6262
);
6363
}
64+
if (view.active) {
65+
return (
66+
<StatusBox>
67+
<Loader size="small" />
68+
<span>Pending...</span>
69+
</StatusBox>
70+
);
71+
}
6472
return <span>Pending...</span>;
6573
};
6674

@@ -136,9 +144,7 @@ export const ApplyActionsStep = (props: Props) => {
136144

137145
// biome-ignore lint/correctness/useExhaustiveDependencies: keyed on the mutation instances so a status change rebuilds the config and rows show live status
138146
const { mutations, variables } = useMemo(() => {
139-
const configs: MutationConfig[] = [
140-
{ id: 'import-destination-certificate', label: 'Import Destination Certificate', mutation: importCertificate },
141-
];
147+
const configs: MutationConfig[] = [];
142148
const resolvers: VariablesResolvers = {
143149
'import-destination-certificate': () => ({ certificate: props.certificate }),
144150
};
@@ -172,6 +178,12 @@ export const ApplyActionsStep = (props: Props) => {
172178
configs.push({ id: CONFIGURATOR_CHAIN_LINK_ID, label: 'Configure Destination', mutation: setup });
173179
resolvers[CONFIGURATOR_CHAIN_LINK_ID] = () => body;
174180

181+
configs.push({
182+
id: 'import-destination-certificate',
183+
label: 'Import Certificate into Truststore',
184+
mutation: importCertificate,
185+
});
186+
175187
configs.push({ id: 'create-location', label: 'Create Location', mutation: createLocation });
176188
resolvers['create-location'] = (prev) => {
177189
const result = configuratorResult(prev);
@@ -253,6 +265,7 @@ export const ApplyActionsStep = (props: Props) => {
253265
sourceBucketName: sourceBucketName ?? '',
254266
targetBucketName: props.targetBucketName ?? '',
255267
destinationAccountName: destinationAccountName ?? '',
268+
isManagementNetwork: props.connectionMode === 'management-network',
256269
},
257270
{ configuratorEvents: setup.events, chainStatusById, configuratorError },
258271
),
@@ -263,6 +276,7 @@ export const ApplyActionsStep = (props: Props) => {
263276
sourceBucketName,
264277
props.targetBucketName,
265278
destinationAccountName,
279+
props.connectionMode,
266280
setup.events,
267281
chainStatusById,
268282
configuratorError,

src/react/locations/CRRSetupWizard/steps/ApplyActionsStep/steps.test.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const baseInput: StepListInput = {
1616
sourceBucketName: 'src-bucket',
1717
targetBucketName: 'target-bucket',
1818
destinationAccountName: 'dest-account',
19+
isManagementNetwork: true,
1920
};
2021

2122
const noProgress: StepStateSources = { configuratorEvents: [], chainStatusById: {} };
@@ -34,7 +35,6 @@ describe('buildStepViews', () => {
3435
it('lists the full provisioning sequence in order when the user creates a new source account and a replication rule', () => {
3536
const views = buildStepViews(baseInput, noProgress);
3637
expect(views.map((v) => v.id)).toEqual([
37-
'import-destination-certificate',
3838
'create-source-account',
3939
'create-source-bucket',
4040
'create-account',
@@ -44,17 +44,24 @@ describe('buildStepViews', () => {
4444
'create-role',
4545
'attach-role-policy',
4646
'create-bucket',
47+
'register-endpoint',
48+
'configure-source-dns',
49+
'import-destination-certificate',
4750
'create-location',
4851
'create-replication-rule',
4952
]);
5053
});
5154

5255
it('numbers the steps and shows the chosen source and destination account names', () => {
53-
const [first, sourceAcc, sourceBkt, destAcc] = buildStepViews(baseInput, noProgress);
54-
expect(first).toMatchObject({ step: 1, label: 'Import Destination Certificate' });
55-
expect(sourceAcc).toMatchObject({ step: 2, label: 'Create Account on Source: src-account' });
56-
expect(sourceBkt).toMatchObject({ step: 3, label: 'Create Bucket on Source: src-bucket' });
57-
expect(destAcc).toMatchObject({ step: 4, label: 'Create Account on Destination: dest-account' });
56+
const views = buildStepViews(baseInput, noProgress);
57+
const [sourceAcc, sourceBkt, destAcc] = views;
58+
expect(sourceAcc).toMatchObject({ step: 1, label: 'Create Account on Source: src-account' });
59+
expect(sourceBkt).toMatchObject({ step: 2, label: 'Create Bucket on Source: src-bucket' });
60+
expect(destAcc).toMatchObject({ step: 3, label: 'Create Account on Destination: dest-account' });
61+
expect(views.find((v) => v.id === 'import-destination-certificate')).toMatchObject({
62+
step: 12,
63+
label: 'Import Certificate into Truststore',
64+
});
5865
});
5966

6067
it('labels the destination IAM chain and the target bucket per the ARTESCA CRR procedure', () => {
@@ -65,6 +72,8 @@ describe('buildStepViews', () => {
6572
expect(labels).toContain('Create IAM Role');
6673
expect(labels).toContain('Attach Policy to Role');
6774
expect(labels).toContain('Create Target Bucket: target-bucket');
75+
expect(labels).toContain('Register Destination S3 Endpoint');
76+
expect(labels).toContain('Configure Source DNS Resolution');
6877
expect(labels).toContain('Create Location');
6978
expect(labels).toContain('Create Replication Rule');
7079
});
@@ -81,6 +90,12 @@ describe('buildStepViews', () => {
8190
expect(views.find((v) => v.id === 'create-replication-rule')).toBeUndefined();
8291
});
8392

93+
it('drops the endpoint and DNS steps in data-network mode', () => {
94+
const views = buildStepViews({ ...baseInput, isManagementNetwork: false }, noProgress);
95+
expect(views.find((v) => v.id === 'register-endpoint')).toBeUndefined();
96+
expect(views.find((v) => v.id === 'configure-source-dns')).toBeUndefined();
97+
});
98+
8499
it('shows every step as pending before anything runs', () => {
85100
expect(buildStepViews(baseInput, noProgress).every((v) => v.state === 'pending')).toBe(true);
86101
});
@@ -149,6 +164,35 @@ describe('buildStepViews — wizard-run rows', () => {
149164
});
150165
});
151166

167+
describe('buildStepViews — active flag', () => {
168+
it('marks a wizard row active only while its link is running', () => {
169+
const running = buildStepViews(baseInput, withChain({ 'create-location': { status: 'pending' } }));
170+
expect(running.find((v) => v.id === 'create-location')).toMatchObject({ state: 'pending', active: true });
171+
});
172+
173+
it('does not mark a wizard row active before it starts or after it succeeds', () => {
174+
const idle = buildStepViews(baseInput, noProgress);
175+
expect(idle.find((v) => v.id === 'create-location')).toMatchObject({ state: 'pending', active: false });
176+
177+
const done = buildStepViews(baseInput, withChain({ 'create-location': { status: 'success' } }));
178+
expect(done.find((v) => v.id === 'create-location')?.active).toBe(false);
179+
});
180+
181+
it('marks a configurator row active between its started and completed events', () => {
182+
const started = buildStepViews(baseInput, withEvents([{ event: 'step.started', step: 'create-user', at: 't' }]));
183+
expect(started.find((v) => v.id === 'create-user')).toMatchObject({ state: 'pending', active: true });
184+
185+
const completed = buildStepViews(
186+
baseInput,
187+
withEvents([
188+
{ event: 'step.started', step: 'create-user', at: 't' },
189+
{ event: 'step.completed', step: 'create-user', at: 't' },
190+
]),
191+
);
192+
expect(completed.find((v) => v.id === 'create-user')).toMatchObject({ state: 'succeeded', active: false });
193+
});
194+
});
195+
152196
describe('buildStepViews — crr-configurator rows', () => {
153197
it('marks a configurator step done once it completes and shows the reason when one fails', () => {
154198
const events: SetupEvent[] = [
@@ -167,6 +211,29 @@ describe('buildStepViews — crr-configurator rows', () => {
167211
expect(failed?.errorMessage).toBe('IAM refused CreateUser: entity already exists');
168212
});
169213

214+
it('drives the management-network endpoint and DNS rows from their stream events', () => {
215+
const succeeded = buildStepViews(
216+
baseInput,
217+
withEvents([{ event: 'step.completed', step: 'register-endpoint', at: 't' }]),
218+
);
219+
expect(succeeded.find((v) => v.id === 'register-endpoint')?.state).toBe('succeeded');
220+
221+
const failed = buildStepViews(
222+
baseInput,
223+
withEvents([
224+
{
225+
event: 'step.failed',
226+
step: 'configure-source-dns',
227+
at: 't',
228+
error: { code: 'InternalError', message: 'source could not resolve the endpoint' },
229+
},
230+
]),
231+
);
232+
const dns = failed.find((v) => v.id === 'configure-source-dns');
233+
expect(dns?.state).toBe('failed');
234+
expect(dns?.errorMessage).toBe('source could not resolve the endpoint');
235+
});
236+
170237
it('blames the first pending configurator row when the stream fails without pinning a step', () => {
171238
const views = buildStepViews(baseInput, {
172239
configuratorEvents: [],
@@ -234,6 +301,8 @@ describe('allSucceeded / hasFailure', () => {
234301
'create-role',
235302
'attach-role-policy',
236303
'create-bucket',
304+
'register-endpoint',
305+
'configure-source-dns',
237306
].map((step) => ({ event: 'step.completed', step, at: 't' }) as SetupEvent),
238307
chainStatusById: {
239308
'import-destination-certificate': { status: 'success' },

0 commit comments

Comments
 (0)