Skip to content

Commit 9c66258

Browse files
committed
feat(secret-manager): correct automatic/assume roles behavior, add information on deletion helper, copy updates
1 parent d931317 commit 9c66258

File tree

8 files changed

+174
-38
lines changed

8 files changed

+174
-38
lines changed

apps/console-v5/src/routes/_authenticated/organization/$organizationId/cluster/$clusterId/settings/addons.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type CheckedState } from '@radix-ui/react-checkbox'
12
import { createFileRoute, useParams } from '@tanstack/react-router'
23
import { type CloudProvider, type ClusterRegion, ServiceTypeEnum } from 'qovery-typescript-axios'
34
import { useEffect, useMemo, useState } from 'react'
@@ -15,6 +16,7 @@ import { useUserRole } from '@qovery/shared/iam/feature'
1516
import {
1617
Badge,
1718
Button,
19+
Checkbox,
1820
DropdownMenu,
1921
Icon,
2022
IconFlag,
@@ -189,6 +191,7 @@ function SecretManagerDeletionHelperModal({
189191
}) {
190192
const [selectedAction, setSelectedAction] = useState<DeletionAction | null>(null)
191193
const [targetId, setTargetId] = useState('')
194+
const [autoDeployImpactedServices, setAutoDeployImpactedServices] = useState<CheckedState>(false)
192195

193196
const hasOtherManagers = otherManagers.length > 0
194197
const hasMultipleManagers = otherManagers.length > 1
@@ -208,19 +211,19 @@ function SecretManagerDeletionHelperModal({
208211
Boolean(selectedAction) && (!hasMultipleManagers || selectedAction !== 'migrate' || Boolean(targetId))
209212

210213
const cardBase =
211-
'flex w-full items-center gap-3 rounded-lg bg-background p-3 text-left outline outline-1 focus:outline focus:outline-1 shadow-[0_0_4px_0_rgba(0,0,0,0.01),0_2px_3px_0_rgba(0,0,0,0.02)]'
214+
'flex w-full items-center gap-3 rounded-lg bg-surface-neutral p-3 text-left outline outline-1 focus:outline focus:outline-1 shadow-[0_0_4px_0_rgba(0,0,0,0.01),0_2px_3px_0_rgba(0,0,0,0.02)]'
212215
const iconBase = 'flex h-10 w-10 items-center justify-center rounded-md'
213216

214217
return (
215218
<div className="relative flex flex-col">
216219
<div className="px-5 pt-5">
217-
<h2 className="text-lg font-medium text-neutral">Deletion helper</h2>
220+
<h2 className="text-lg font-medium text-neutral">Choose how to handle existing secrets</h2>
218221
<p className="mt-1 text-sm text-neutral-subtle">
219-
"{integration.name}" is currently used by {integration.usedByServices ?? 0} services. Choose what you want to
220-
do with the linked external secrets before before deleting it.
222+
"{integration.name}" is currently used by {integration.usedByServices ?? 0} services. To finalize the
223+
deletion, you'll need to redeploy all impacted services before deploying your cluster.
221224
</p>
222225
</div>
223-
<div className="flex flex-col gap-2 px-5 py-5">
226+
<div className="flex flex-col gap-3 px-5 py-5">
224227
{hasOtherManagers && hasMultipleManagers ? (
225228
<div className="flex flex-col gap-0 rounded-lg border border-neutral bg-surface-neutral-subtle">
226229
<button
@@ -241,7 +244,7 @@ function SecretManagerDeletionHelperModal({
241244
>
242245
<Icon iconName="right-left" className={selectedAction === 'migrate' ? 'text-brand' : undefined} />
243246
</div>
244-
<div className="flex flex-col gap-1">
247+
<div className="flex flex-col gap-0.5">
245248
<span className="text-sm font-medium text-neutral">Migrate to another secret manager</span>
246249
<span className="text-xs text-neutral-subtle">
247250
Migration to one of your other secret manager detected
@@ -281,7 +284,7 @@ function SecretManagerDeletionHelperModal({
281284
>
282285
<Icon iconName="right-left" className={selectedAction === 'migrate' ? 'text-brand' : undefined} />
283286
</div>
284-
<div className="flex flex-col gap-1">
287+
<div className="flex flex-col gap-0.5">
285288
<span className="text-sm font-medium text-neutral">Migrate to detected secret manager</span>
286289
<span className="text-xs text-neutral-subtle">References will point to "{otherManagers[0]?.name}"</span>
287290
</div>
@@ -305,7 +308,7 @@ function SecretManagerDeletionHelperModal({
305308
>
306309
<Icon iconName="link-broken" className={selectedAction === 'detach' ? 'text-brand' : undefined} />
307310
</div>
308-
<div className="flex flex-col gap-1">
311+
<div className="flex flex-col gap-0.5">
309312
<span className="text-sm font-medium text-neutral">Detach all references</span>
310313
<span className="text-xs text-neutral-subtle">Empty external secrets to be remapped later</span>
311314
</div>
@@ -327,11 +330,24 @@ function SecretManagerDeletionHelperModal({
327330
>
328331
<Icon iconName="lock-keyhole" className={selectedAction === 'convert' ? 'text-brand' : undefined} />
329332
</div>
330-
<div className="flex flex-col gap-1">
333+
<div className="flex flex-col gap-0.5">
331334
<span className="text-sm font-medium text-neutral">Convert to empty Qovery secrets</span>
332335
<span className="text-xs text-neutral-subtle">Conversion to empty qovery secrets for manual migration</span>
333336
</div>
334337
</button>
338+
339+
<div className="mt-1 flex items-start gap-2">
340+
<Checkbox
341+
id="auto-deploy-impacted-services"
342+
name="auto-deploy-impacted-services"
343+
checked={autoDeployImpactedServices}
344+
onCheckedChange={setAutoDeployImpactedServices}
345+
className="mt-0.5 shrink-0"
346+
/>
347+
<label htmlFor="auto-deploy-impacted-services" className="text-sm text-neutral">
348+
Automatically deploy impacted services
349+
</label>
350+
</div>
335351
</div>
336352
<div className="flex items-center justify-end gap-3 border-t border-neutral px-5 py-4">
337353
<Button type="button" variant="plain" color="neutral" size="lg" onClick={onClose}>

apps/console-v5/src/routes/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/variables.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ function RouteComponent() {
3232
options: EXTERNAL_SECRETS_USE_CASES,
3333
defaultCaseId: 'filled',
3434
})
35+
const hasClusterSecretManagerConfigured =
36+
selectedCaseId === 'filled' ||
37+
selectedCaseId === 'empty' ||
38+
selectedCaseId === 'secret-manager-addon-not-redeployed'
3539

3640
const { data: service } = useService({
3741
environmentId,
@@ -113,6 +117,7 @@ function RouteComponent() {
113117
environmentId={environmentId}
114118
serviceId={serviceId}
115119
toasterCallback={toasterCallback}
120+
hasClusterSecretManagerConfigured={hasClusterSecretManagerConfigured}
116121
/>
117122
)}
118123
{activeTab === 'external-secrets' && (
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
2+
import {
3+
SecretManagerIntegrationModal,
4+
type SecretManagerIntegrationModalProps,
5+
} from './secret-manager-integration-modal'
6+
7+
const defaultProps: SecretManagerIntegrationModalProps = {
8+
option: {
9+
value: 'aws-manager',
10+
label: 'AWS Secret manager',
11+
icon: 'AWS',
12+
typeLabel: 'AWS Secret manager',
13+
},
14+
regionOptions: [{ label: 'Paris (eu-west-3)', value: 'eu-west-3' }],
15+
clusterProvider: 'AWS',
16+
onClose: jest.fn(),
17+
onSubmit: jest.fn(),
18+
}
19+
20+
const getAutomaticTab = () => {
21+
const automaticIntegrationLabel = screen.getByText('Automatic integration')
22+
return (automaticIntegrationLabel.closest('[aria-disabled="true"]') ??
23+
automaticIntegrationLabel.closest('[aria-disabled]')) as HTMLElement
24+
}
25+
26+
describe('SecretManagerIntegrationModal', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks()
29+
})
30+
31+
it('should force static credentials when an automatic integration already exists', async () => {
32+
const { userEvent } = renderWithProviders(
33+
<SecretManagerIntegrationModal {...defaultProps} hasAwsAutomaticIntegrationConfigured />
34+
)
35+
36+
expect(screen.getByLabelText('Authentication type')).toBeDisabled()
37+
expect(screen.getByText('Static credentials')).toBeInTheDocument()
38+
39+
const automaticTab = getAutomaticTab()
40+
expect(automaticTab).toHaveAttribute('aria-disabled')
41+
42+
await userEvent.hover(automaticTab)
43+
44+
expect(
45+
(
46+
await screen.findAllByText(
47+
'Static credentials are the only available option while Automatic integration is configured'
48+
)
49+
).length
50+
).toBeGreaterThan(0)
51+
})
52+
53+
it('should force static credentials when an STS integration already exists', async () => {
54+
const { userEvent } = renderWithProviders(
55+
<SecretManagerIntegrationModal {...defaultProps} hasAwsManualStsIntegrationConfigured />
56+
)
57+
58+
expect(screen.getByLabelText('Authentication type')).toBeDisabled()
59+
expect(screen.getByText('Static credentials')).toBeInTheDocument()
60+
61+
const automaticTab = getAutomaticTab()
62+
expect(automaticTab).toHaveAttribute('aria-disabled')
63+
64+
await userEvent.hover(automaticTab)
65+
66+
expect(
67+
(
68+
await screen.findAllByText(
69+
'Static credentials are the only available option while Assume role integration is configured'
70+
)
71+
).length
72+
).toBeGreaterThan(0)
73+
})
74+
})

libs/domains/clusters/feature/src/lib/secret-manager-modals/secret-manager-integration-modal.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ type SecretManagerIntegrationFormValues = {
3737
const AUTOMATIC_INTEGRATION_DISABLED_TOOLTIP =
3838
'Automatic integration is unavailable because an STS manual integration is already configured.'
3939

40-
const STATIC_CREDENTIALS_DISABLED_TOOLTIP =
41-
'Static credentials are the only available option while automatic integration is configured'
42-
4340
export interface SecretManagerIntegrationModalProps {
4441
option: SecretManagerOption
4542
regionOptions: Array<{ label: string; value: string; icon?: JSX.Element }>
@@ -65,15 +62,25 @@ export function SecretManagerIntegrationModal({
6562
}: SecretManagerIntegrationModalProps) {
6663
const isAwsCluster = clusterProvider === 'AWS'
6764
const isAwsIntegration = option.icon === 'AWS'
68-
const isAwsAutomaticIntegrationBlockedByExistingRoleArn =
69-
isAwsCluster && isAwsIntegration && hasAwsManualStsIntegrationConfigured
70-
const isAwsStaticCredentialsBlockedByExistingAutomatic =
71-
isAwsCluster && isAwsIntegration && hasAwsAutomaticIntegrationConfigured
65+
const hasConfiguredAwsAutomaticIntegration = isAwsCluster && isAwsIntegration && hasAwsAutomaticIntegrationConfigured
66+
const hasConfiguredAwsManualStsIntegration = isAwsCluster && isAwsIntegration && hasAwsManualStsIntegrationConfigured
67+
const blockedAwsIntegrationLabel = hasConfiguredAwsAutomaticIntegration
68+
? 'Automatic'
69+
: hasConfiguredAwsManualStsIntegration
70+
? 'Assume role'
71+
: undefined
72+
const isEditingAwsManualStsIntegration =
73+
mode === 'edit' && initialValues?.authentication === 'Manual' && initialValues?.authType === 'sts'
74+
const shouldForceStaticCredentials = Boolean(blockedAwsIntegrationLabel) && !isEditingAwsManualStsIntegration
75+
const shouldForceStsCredentials = isEditingAwsManualStsIntegration
76+
const showAutomaticTabFirst = !blockedAwsIntegrationLabel
77+
const disabledIntegrationTooltip =
78+
shouldForceStaticCredentials && blockedAwsIntegrationLabel
79+
? `Static credentials are the only available option while ${blockedAwsIntegrationLabel} integration is configured`
80+
: AUTOMATIC_INTEGRATION_DISABLED_TOOLTIP
7281

7382
const [activeTab, setActiveTab] = useState<IntegrationTab>(() =>
74-
initialValues?.authentication === 'Manual' || isAwsAutomaticIntegrationBlockedByExistingRoleArn
75-
? 'manual'
76-
: 'automatic'
83+
initialValues?.authentication === 'Manual' || Boolean(blockedAwsIntegrationLabel) ? 'manual' : 'automatic'
7784
)
7885
const methods = useForm<SecretManagerIntegrationFormValues>({
7986
mode: 'onChange',
@@ -90,13 +97,13 @@ export function SecretManagerIntegrationModal({
9097

9198
const authenticationOptions = useMemo(
9299
() =>
93-
isAwsStaticCredentialsBlockedByExistingAutomatic
100+
shouldForceStaticCredentials
94101
? [{ label: 'Static credentials', value: 'static' }]
95102
: [
96103
{ label: 'Assume role via STS', value: 'sts' },
97104
{ label: 'Static credentials', value: 'static' },
98105
],
99-
[isAwsStaticCredentialsBlockedByExistingAutomatic]
106+
[shouldForceStaticCredentials]
100107
)
101108

102109
const authenticationType = methods.watch('authenticationType')
@@ -107,9 +114,6 @@ export function SecretManagerIntegrationModal({
107114
const isManualOnlyGcpIntegration = isGcpSecretManagerOnAws
108115
const isManualOnlyAwsIntegration = isAwsSecretManagerOnGcp
109116
const isGcpManualTabOnGcpSecretManager = option.value === 'gcp-secret' && isGcpCluster && activeTab === 'manual'
110-
const shouldForceStaticCredentials = isAwsStaticCredentialsBlockedByExistingAutomatic
111-
const shouldForceStsCredentials = isAwsAutomaticIntegrationBlockedByExistingRoleArn
112-
const showAutomaticTabFirst = !isAwsAutomaticIntegrationBlockedByExistingRoleArn
113117
const { getRootProps, getInputProps, isDragActive } = useDropzone({
114118
multiple: false,
115119
accept: { 'application/json': ['.json'] },
@@ -331,7 +335,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account"
331335

332336
if (shouldForceStaticCredentials) {
333337
return (
334-
<Tooltip content={STATIC_CREDENTIALS_DISABLED_TOOLTIP}>
338+
<Tooltip content={disabledIntegrationTooltip}>
335339
<div className="w-full">{authenticationTypeSelect}</div>
336340
</Tooltip>
337341
)
@@ -552,7 +556,7 @@ bash -s -- $GOOGLE_CLOUD_PROJECT qovery_role qovery-service-account"
552556
<Icon iconName="hammer" iconStyle="regular" />
553557
Manual integration
554558
</Navbar.Item>
555-
<Tooltip content={AUTOMATIC_INTEGRATION_DISABLED_TOOLTIP}>
559+
<Tooltip content={disabledIntegrationTooltip}>
556560
<Navbar.Item
557561
id="automatic"
558562
aria-disabled

libs/domains/environments/feature/src/lib/create-clone-environment-modal/create-clone-environment-modal.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ function CloneMigrationHelperModal({
109109
return (
110110
<div className="relative flex flex-col">
111111
<div className="px-5 pt-5">
112-
<h2 className="text-lg font-medium text-neutral">Migration helper</h2>
112+
<h2 className="text-lg font-medium text-neutral">Choose how to migrate existing secrets</h2>
113113
<p className="mt-1 text-sm text-neutral-subtle">
114114
You’re about to clone a environment that contains external secrets from a secret manager on a new cluster.
115-
Please choose you’re preferred options.
115+
Please choose your preferred options.
116116
</p>
117117
</div>
118118
<div className="flex flex-col gap-2 px-5 py-5">
@@ -141,7 +141,7 @@ function CloneMigrationHelperModal({
141141
className={selectedAction === 'migrate' ? 'text-brand' : undefined}
142142
/>
143143
</div>
144-
<div className="flex flex-col gap-1">
144+
<div className="flex flex-col gap-0.5">
145145
<span className="text-sm font-medium text-neutral">Migrate to another secret manager</span>
146146
<span className="text-xs text-neutral-subtle">
147147
Migration to one of your other secret manager detected
@@ -168,7 +168,7 @@ function CloneMigrationHelperModal({
168168
<div className={`${iconBase} bg-surface-neutral-component`}>
169169
<Icon iconName="right-left" iconStyle="regular" />
170170
</div>
171-
<div className="flex flex-col gap-1">
171+
<div className="flex flex-col gap-0.5">
172172
<span className="text-sm font-medium text-neutral">Migrate to another secret manager</span>
173173
<span className="text-xs text-neutral-subtle">
174174
Migration to one of your other secret manager detected
@@ -197,7 +197,7 @@ function CloneMigrationHelperModal({
197197
className={selectedAction === 'migrate' ? 'text-brand' : undefined}
198198
/>
199199
</div>
200-
<div className="flex flex-col gap-1">
200+
<div className="flex flex-col gap-0.5">
201201
<span className="text-sm font-medium text-neutral">Migrate to detected secret manager</span>
202202
<span className="text-xs text-neutral-subtle">
203203
Migration to “
@@ -227,7 +227,7 @@ function CloneMigrationHelperModal({
227227
className={selectedAction === 'detach' ? 'text-brand' : undefined}
228228
/>
229229
</div>
230-
<div className="flex flex-col gap-1">
230+
<div className="flex flex-col gap-0.5">
231231
<span className="text-sm font-medium text-neutral">Detach all references</span>
232232
<span className="text-xs text-neutral-subtle">Empty external secrets to be remapped later</span>
233233
</div>
@@ -253,7 +253,7 @@ function CloneMigrationHelperModal({
253253
className={selectedAction === 'convert' ? 'text-brand' : undefined}
254254
/>
255255
</div>
256-
<div className="flex flex-col gap-1">
256+
<div className="flex flex-col gap-0.5">
257257
<span className="text-sm font-medium text-neutral">Convert to empty Qovery secrets</span>
258258
<span className="text-xs text-neutral-subtle">Conversion to empty qovery secrets for manual migration</span>
259259
</div>
@@ -314,7 +314,7 @@ function CloneMigrationTableModal({
314314
return (
315315
<div className="relative flex flex-col">
316316
<div className="px-5 pt-5">
317-
<h2 className="text-lg font-medium text-neutral">Migration helper</h2>
317+
<h2 className="text-lg font-medium text-neutral">Choose how to migrate existing secrets</h2>
318318
<p className="mt-1 text-sm text-neutral-subtle">
319319
You’re about to clone a environment that contains external secrets from a secret manager on a new cluster. For
320320
each detected secret manager please choose you’re preferred option.

0 commit comments

Comments
 (0)