mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
feat(secret-backend): add Workload Identity Federation for Azure Key Vault (#9061)
* [ee] feat(secret-backend): add Workload Identity Federation for Azure Key Vault Make `client_secret` optional. When omitted, Windmill falls back to Azure Workload Identity Federation: it reads the projected service-account JWT from AZURE_FEDERATED_TOKEN_FILE and exchanges it with Entra ID via `client_assertion`, no long-lived secret stored on the instance. Same code path covers AKS (workload-identity admission webhook auto-injects the env vars) and any other Kubernetes cluster federated to Entra ID (EKS/GKE/self-hosted). - backend: relax client_secret to Option (already was), update doc comment + OpenAPI description; the actual auth-branching logic lives in the EE companion file (azure_kv_ee.rs). - frontend: drop client_secret/token from canSubmit so saving with an empty secret is allowed; add inline help under the Client Secret field pointing to AZURE_FEDERATED_TOKEN_FILE; mark the field optional. - ee-repo-ref: bump to the EE companion commit. EE companion: see windmill-ee-private branch azure-keyvault-managed-identity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [ee] chore: bump ee-repo-ref for blank-client_secret fix Picks up the EE-side fix (windmill-ee-private c7c0a23) that treats blank `client_secret` as workload-identity instead of POSTing an empty string to Entra ID. Addresses Codex review on PR #9061. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to c8d100d74b8de6bd26fc973d5edbd8853d54dd8b This commit updates the EE repository reference after PR #561 was merged in windmill-ee-private. Previous ee-repo-ref: c7c0a23459b0e7416a045a279346cc48b30eed32 New ee-repo-ref: c8d100d74b8de6bd26fc973d5edbd8853d54dd8b Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
co-authored by
Claude Opus 4.7
windmill-internal-app[bot]
Ruben Fiszel
parent
9de38f9a09
commit
0c203e8cf1
@@ -1 +1 @@
|
||||
c6d55247a7fd951f561e0b2ad2ac13051274aa77
|
||||
c8d100d74b8de6bd26fc973d5edbd8853d54dd8b
|
||||
|
||||
@@ -33961,7 +33961,7 @@
|
||||
},
|
||||
"client_secret": {
|
||||
"type": "string",
|
||||
"description": "Azure AD client secret"
|
||||
"description": "Azure AD client secret. Optional — when omitted, the integration falls back to Azure Workload Identity Federation, exchanging the Kubernetes-projected service-account JWT at AZURE_FEDERATED_TOKEN_FILE for an access token (no long-lived secret stored)."
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
|
||||
@@ -2977,7 +2977,12 @@ paths:
|
||||
description: Azure AD application (client) ID
|
||||
client_secret:
|
||||
type: string
|
||||
description: Azure AD client secret
|
||||
description: >-
|
||||
Azure AD client secret. Optional — when omitted, the
|
||||
integration falls back to Azure Workload Identity Federation,
|
||||
exchanging the Kubernetes-projected service-account JWT at
|
||||
AZURE_FEDERATED_TOKEN_FILE for an access token (no long-lived
|
||||
secret stored).
|
||||
token:
|
||||
type: string
|
||||
description: >-
|
||||
|
||||
@@ -20929,7 +20929,10 @@ components:
|
||||
description: Azure AD application (client) ID
|
||||
client_secret:
|
||||
type: string
|
||||
description: Azure AD client secret
|
||||
description: >-
|
||||
Azure AD client secret. Optional — when omitted, the integration falls back to
|
||||
Azure Workload Identity Federation, exchanging the Kubernetes-projected service-account
|
||||
JWT at AZURE_FEDERATED_TOKEN_FILE for an access token (no long-lived secret stored).
|
||||
token:
|
||||
type: string
|
||||
description: Static Bearer token for testing/development (optional, if provided this is used instead of OAuth2 authentication)
|
||||
|
||||
@@ -143,7 +143,10 @@ pub struct AzureKeyVaultSettings {
|
||||
pub tenant_id: String,
|
||||
/// Azure AD application (client) ID
|
||||
pub client_id: String,
|
||||
/// Azure AD client secret
|
||||
/// Azure AD client secret. Optional — when omitted, the integration falls back to
|
||||
/// Azure Workload Identity Federation: the Kubernetes-projected service-account JWT at
|
||||
/// `AZURE_FEDERATED_TOKEN_FILE` is exchanged with Entra ID for an access token (no
|
||||
/// long-lived secret stored on the Windmill instance).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_secret: Option<String>,
|
||||
/// Static Bearer token for testing/development (optional)
|
||||
|
||||
@@ -26,9 +26,8 @@
|
||||
}
|
||||
})
|
||||
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' | 'AwsSecretsManager' = $derived(
|
||||
$values['secret_backend']?.type ?? 'Database'
|
||||
)
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' | 'AwsSecretsManager' =
|
||||
$derived($values['secret_backend']?.type ?? 'Database')
|
||||
|
||||
let authMethod: 'token' | 'jwt' = $derived.by(() => {
|
||||
const config = $values['secret_backend']
|
||||
@@ -58,7 +57,11 @@
|
||||
|
||||
function setBackendType(type: string | undefined) {
|
||||
if (!type) return
|
||||
if ((type === 'HashiCorpVault' || type === 'AzureKeyVault' || type === 'AwsSecretsManager') && vaultDisabled) return
|
||||
if (
|
||||
(type === 'HashiCorpVault' || type === 'AzureKeyVault' || type === 'AwsSecretsManager') &&
|
||||
vaultDisabled
|
||||
)
|
||||
return
|
||||
if (type === 'Database') {
|
||||
$values['secret_backend'] = { type: 'Database' }
|
||||
} else if (type === 'HashiCorpVault') {
|
||||
@@ -93,11 +96,24 @@
|
||||
}
|
||||
|
||||
function setAuthMethod(method: string | undefined) {
|
||||
if (!method || !$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
if (
|
||||
!method ||
|
||||
!$values['secret_backend'] ||
|
||||
$values['secret_backend'].type !== 'HashiCorpVault'
|
||||
)
|
||||
return
|
||||
if (method === 'token') {
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], jwt_role: null, token: $values['secret_backend'].token ?? '' }
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
jwt_role: null,
|
||||
token: $values['secret_backend'].token ?? ''
|
||||
}
|
||||
} else if (method === 'jwt') {
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], token: null, jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets' }
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
token: null,
|
||||
jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +136,9 @@
|
||||
sendUserToast('Successfully connected to HashiCorp Vault')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to Vault: ' + error.message, true)
|
||||
} finally { testingConnection = false }
|
||||
} finally {
|
||||
testingConnection = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsToVault() {
|
||||
@@ -128,25 +146,48 @@
|
||||
migratingToVault = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToVault({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToVault = false; migrateToVaultModalOpen = false }
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToVault = false
|
||||
migrateToVaultModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsToDatabase() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
migratingToDatabase = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToDatabase({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToDatabase = false; migrateToDatabaseModalOpen = false }
|
||||
const report = await SettingService.migrateSecretsToDatabase({
|
||||
requestBody: getVaultSettings()
|
||||
})
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(
|
||||
`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`
|
||||
)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToDatabase = false
|
||||
migrateToDatabaseModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
function isVaultConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return false
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault')
|
||||
return false
|
||||
const hasAddress = $values['secret_backend'].address?.trim() !== ''
|
||||
const hasMountPath = $values['secret_backend'].mount_path?.trim() !== ''
|
||||
const hasToken = $values['secret_backend'].token?.trim()
|
||||
@@ -170,39 +211,68 @@
|
||||
try {
|
||||
await SettingService.testAzureKvBackend({ requestBody: getAzureKvSettings() })
|
||||
sendUserToast('Successfully connected to Azure Key Vault')
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAzureKvConnection = false }
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
testingAzureKvConnection = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsToAzureKv() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AzureKeyVault') return
|
||||
migratingToAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAzureKv = false; migrateToAzureKvModalOpen = false }
|
||||
const report = await SettingService.migrateSecretsToAzureKv({
|
||||
requestBody: getAzureKvSettings()
|
||||
})
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(
|
||||
`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`
|
||||
)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToAzureKv = false
|
||||
migrateToAzureKvModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAzureKv() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AzureKeyVault') return
|
||||
migratingFromAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAzureKv = false; migrateFromAzureKvModalOpen = false }
|
||||
const report = await SettingService.migrateSecretsFromAzureKv({
|
||||
requestBody: getAzureKvSettings()
|
||||
})
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(
|
||||
`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`
|
||||
)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingFromAzureKv = false
|
||||
migrateFromAzureKvModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
function isAzureKvConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AzureKeyVault') return false
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AzureKeyVault')
|
||||
return false
|
||||
return (
|
||||
$values['secret_backend'].vault_url?.trim() !== '' &&
|
||||
$values['secret_backend'].tenant_id?.trim() !== '' &&
|
||||
$values['secret_backend'].client_id?.trim() !== '' &&
|
||||
(!!$values['secret_backend'].client_secret?.trim() || !!$values['secret_backend'].token?.trim())
|
||||
$values['secret_backend'].client_id?.trim() !== ''
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,8 +292,11 @@
|
||||
try {
|
||||
await SettingService.testAwsSmBackend({ requestBody: getAwsSmSettings() })
|
||||
sendUserToast('Successfully connected to AWS Secrets Manager')
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAwsSmConnection = false }
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
testingAwsSmConnection = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsToAwsSm() {
|
||||
@@ -231,25 +304,50 @@
|
||||
migratingToAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to AWS Secrets Manager`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAwsSm = false; migrateToAwsSmModalOpen = false }
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(
|
||||
`Migrated ${report.migrated_count}/${report.total_secrets} secrets to AWS Secrets Manager`
|
||||
)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToAwsSm = false
|
||||
migrateToAwsSmModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAwsSm() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
migratingFromAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAwsSm = false; migrateFromAwsSmModalOpen = false }
|
||||
const report = await SettingService.migrateSecretsFromAwsSm({
|
||||
requestBody: getAwsSmSettings()
|
||||
})
|
||||
if (report.failed_count > 0)
|
||||
sendUserToast(
|
||||
`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`,
|
||||
true
|
||||
)
|
||||
else
|
||||
sendUserToast(
|
||||
`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`
|
||||
)
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed: ' + error.message, true)
|
||||
} finally {
|
||||
migratingFromAwsSm = false
|
||||
migrateFromAwsSmModalOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
function isAwsSmConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return false
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager')
|
||||
return false
|
||||
return $values['secret_backend'].region?.trim() !== ''
|
||||
}
|
||||
|
||||
@@ -260,10 +358,39 @@
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<ToggleButtonGroup selected={selectedType} onSelected={(v) => setBackendType(v)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton value="Database" label="Database" tooltip="Store secrets encrypted in the database (default)" item={toggleButton} />
|
||||
<ToggleButton value="HashiCorpVault" label="HashiCorp Vault (Beta)" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in HashiCorp Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AzureKeyVault" label="Azure Key Vault" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in Azure Key Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AwsSecretsManager" label="AWS Secrets Manager (Beta)" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in AWS Secrets Manager'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton
|
||||
value="Database"
|
||||
label="Database"
|
||||
tooltip="Store secrets encrypted in the database (default)"
|
||||
item={toggleButton}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="HashiCorpVault"
|
||||
label="HashiCorp Vault (Beta)"
|
||||
tooltip={vaultDisabled
|
||||
? 'Requires Enterprise Edition'
|
||||
: 'Store secrets in HashiCorp Vault'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="AzureKeyVault"
|
||||
label="Azure Key Vault"
|
||||
tooltip={vaultDisabled
|
||||
? 'Requires Enterprise Edition'
|
||||
: 'Store secrets in Azure Key Vault'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="AwsSecretsManager"
|
||||
label="AWS Secrets Manager (Beta)"
|
||||
tooltip={vaultDisabled
|
||||
? 'Requires Enterprise Edition'
|
||||
: 'Store secrets in AWS Secrets Manager'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if vaultDisabled}
|
||||
@@ -278,7 +405,10 @@
|
||||
<Database class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">Database Storage (Default)</p>
|
||||
<p class="text-xs text-secondary">Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL database.</p>
|
||||
<p class="text-xs text-secondary"
|
||||
>Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL
|
||||
database.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'HashiCorpVault'}
|
||||
@@ -286,46 +416,101 @@
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Lock class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">HashiCorp Vault Configuration <span class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded">Beta</span></p>
|
||||
<p class="text-xs text-secondary">Store secrets in an external HashiCorp Vault instance.</p>
|
||||
<p class="text-sm font-medium text-emphasis"
|
||||
>HashiCorp Vault Configuration <span
|
||||
class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded"
|
||||
>Beta</span
|
||||
></p
|
||||
>
|
||||
<p class="text-xs text-secondary"
|
||||
>Store secrets in an external HashiCorp Vault instance.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis">Vault Address</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_address', placeholder: 'https://vault.company.com:8200', disabled }} bind:value={$values['secret_backend'].address} />
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Address</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_address',
|
||||
placeholder: 'https://vault.company.com:8200',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].address}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis">KV Mount Path</label>
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis"
|
||||
>KV Mount Path</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">The KV v2 secrets engine mount path in Vault</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_mount_path', placeholder: 'windmill', disabled }} bind:value={$values['secret_backend'].mount_path} />
|
||||
<TextInput
|
||||
inputProps={{ type: 'text', id: 'vault_mount_path', placeholder: 'windmill', disabled }}
|
||||
bind:value={$values['secret_backend'].mount_path}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="block text-xs font-semibold text-emphasis">Authentication Method</span>
|
||||
<ToggleButtonGroup selected={authMethod} onSelected={(v) => setAuthMethod(v)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton value="jwt" label="JWT Auth" tooltip="Authenticate using Windmill-signed JWTs" item={toggleButton} {disabled} />
|
||||
<ToggleButton value="token" label="Static Token" tooltip="Use a static Vault token" item={toggleButton} {disabled} />
|
||||
<ToggleButton
|
||||
value="jwt"
|
||||
label="JWT Auth"
|
||||
tooltip="Authenticate using Windmill-signed JWTs"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="token"
|
||||
label="Static Token"
|
||||
tooltip="Use a static Vault token"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if authMethod === 'token'}
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis">Vault Token</label>
|
||||
<span class="text-2xs text-secondary">Static token. Recommended only for testing/development.</span>
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Token</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Static token. Recommended only for testing/development.</span
|
||||
>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis">JWT Auth Role</label>
|
||||
<span class="text-2xs text-secondary">The JWT authentication role configured in Vault.</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_jwt_role', placeholder: 'windmill-secrets', disabled }} bind:value={$values['secret_backend'].jwt_role} />
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis"
|
||||
>JWT Auth Role</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>The JWT authentication role configured in Vault.</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_jwt_role',
|
||||
placeholder: 'windmill-secrets',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].jwt_role}
|
||||
/>
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary">Vault JWT Setup Instructions</summary>
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary"
|
||||
>Vault JWT Setup Instructions</summary
|
||||
>
|
||||
<div class="mt-2 p-2 bg-surface rounded text-2xs text-secondary space-y-2">
|
||||
<p>Configure Vault to accept JWTs from Windmill:</p>
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto">
|
||||
<pre># Enable JWT auth method
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto"
|
||||
>
|
||||
<pre
|
||||
># Enable JWT auth method
|
||||
vault auth enable jwt
|
||||
|
||||
# Configure JWT auth with Windmill's JWKS endpoint
|
||||
@@ -349,37 +534,82 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
bound_audiences="{baseUrl}" \
|
||||
user_claim="email" \
|
||||
policies="windmill-secrets" \
|
||||
ttl="1h"</pre>
|
||||
ttl="1h"</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis">Namespace (optional)</label>
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis"
|
||||
>Namespace (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary">Vault Enterprise namespace</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_namespace', placeholder: 'admin/my-namespace', disabled }} bind:value={$values['secret_backend'].namespace} />
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_namespace',
|
||||
placeholder: 'admin/my-namespace',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].namespace}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Toggle id="vault_skip_ssl_verify" {disabled} bind:checked={$values['secret_backend'].skip_ssl_verify} size="xs" options={{ right: 'Skip TLS certificate verification' }} />
|
||||
<span class="text-2xs text-secondary">Disables TLS verification when connecting to Vault. Only enable for self-signed certificates in development.</span>
|
||||
<Toggle
|
||||
id="vault_skip_ssl_verify"
|
||||
{disabled}
|
||||
bind:checked={$values['secret_backend'].skip_ssl_verify}
|
||||
size="xs"
|
||||
options={{ right: 'Skip TLS certificate verification' }}
|
||||
/>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Disables TLS verification when connecting to Vault. Only enable for self-signed
|
||||
certificates in development.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<Button unifiedSize="md" variant="accent" onclick={testVaultConnection} disabled={disabled || !isVaultConfigValid() || testingConnection} loading={testingConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testVaultConnection}
|
||||
disabled={disabled || !isVaultConfigValid() || testingConnection}
|
||||
loading={testingConnection}
|
||||
startIcon={{ icon: Server }}>Test Connection</Button
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Original values are NOT deleted to allow for rollback.</span
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Lock size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Database size={16} /><ArrowRight size={16} /><Lock size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">Database → Vault</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToVaultModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToVault} startIcon={{ icon: ArrowRight }}>Migrate to Vault</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToVaultModalOpen = true)}
|
||||
disabled={disabled || !isVaultConfigValid() || migratingToVault}
|
||||
startIcon={{ icon: ArrowRight }}>Migrate to Vault</Button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Lock size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Lock size={16} /><ArrowLeft size={16} /><Database size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">Vault → Database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToDatabaseModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToDatabase} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToDatabaseModalOpen = true)}
|
||||
disabled={disabled || !isVaultConfigValid() || migratingToDatabase}
|
||||
startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,42 +626,109 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_vault_url" class="block text-xs font-semibold text-emphasis">Vault URL</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_vault_url', placeholder: 'https://my-vault.vault.azure.net', disabled }} bind:value={$values['secret_backend'].vault_url} />
|
||||
<label for="azure_vault_url" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault URL</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_vault_url',
|
||||
placeholder: 'https://my-vault.vault.azure.net',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].vault_url}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_tenant_id" class="block text-xs font-semibold text-emphasis">Tenant ID</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_tenant_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].tenant_id} />
|
||||
<label for="azure_tenant_id" class="block text-xs font-semibold text-emphasis"
|
||||
>Tenant ID</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_tenant_id',
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].tenant_id}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_client_id" class="block text-xs font-semibold text-emphasis">Client ID</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_client_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].client_id} />
|
||||
<label for="azure_client_id" class="block text-xs font-semibold text-emphasis"
|
||||
>Client ID</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'azure_client_id',
|
||||
placeholder: '00000000-0000-0000-0000-000000000000',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].client_id}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_client_secret" class="block text-xs font-semibold text-emphasis">Client Secret</label>
|
||||
<label for="azure_client_secret" class="block text-xs font-semibold text-emphasis"
|
||||
>Client Secret <span class="text-2xs font-normal text-secondary">(optional)</span
|
||||
></label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Leave blank to use Azure Workload Identity. Requires
|
||||
<code>AZURE_FEDERATED_TOKEN_FILE</code> on the Windmill process (auto-injected on AKS by
|
||||
the workload-identity webhook; set manually on other Kubernetes clusters).</span
|
||||
>
|
||||
<Password bind:password={$values['secret_backend'].client_secret} small {disabled} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_token" class="block text-xs font-semibold text-emphasis">Token (optional)</label>
|
||||
<span class="text-2xs text-secondary">Static Bearer token for testing. If provided, OAuth2 is skipped.</span>
|
||||
<label for="azure_token" class="block text-xs font-semibold text-emphasis"
|
||||
>Token (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Static Bearer token for testing. If provided, OAuth2 is skipped.</span
|
||||
>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAzureKvConnection} disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection} loading={testingAzureKvConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testAzureKvConnection}
|
||||
disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection}
|
||||
loading={testingAzureKvConnection}
|
||||
startIcon={{ icon: Server }}>Test Connection</Button
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Original values are NOT deleted to allow for rollback.</span
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">Database → Azure Key Vault</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv} startIcon={{ icon: ArrowRight }}>Migrate to Azure KV</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToAzureKvModalOpen = true)}
|
||||
disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv}
|
||||
startIcon={{ icon: ArrowRight }}>Migrate to Azure KV</Button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">Azure Key Vault → Database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingFromAzureKv} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateFromAzureKvModalOpen = true)}
|
||||
disabled={disabled || !isAzureKvConfigValid() || migratingFromAzureKv}
|
||||
startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,50 +739,118 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Cloud class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">AWS Secrets Manager Configuration <span class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded">Beta</span></p>
|
||||
<p class="text-sm font-medium text-emphasis"
|
||||
>AWS Secrets Manager Configuration <span
|
||||
class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded"
|
||||
>Beta</span
|
||||
></p
|
||||
>
|
||||
<p class="text-xs text-secondary">Store secrets in AWS Secrets Manager.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_region" class="block text-xs font-semibold text-emphasis">Region</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_region', placeholder: 'us-east-1', disabled }} bind:value={$values['secret_backend'].region} />
|
||||
<label for="aws_sm_region" class="block text-xs font-semibold text-emphasis">Region</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{ type: 'text', id: 'aws_sm_region', placeholder: 'us-east-1', disabled }}
|
||||
bind:value={$values['secret_backend'].region}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_access_key_id" class="block text-xs font-semibold text-emphasis">Access Key ID (optional)</label>
|
||||
<span class="text-2xs text-secondary">If not provided, the default AWS credential chain is used (env vars, instance profile, EKS pod identity)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_access_key_id', placeholder: 'AKIA...', disabled }} bind:value={$values['secret_backend'].access_key_id} />
|
||||
<label for="aws_sm_access_key_id" class="block text-xs font-semibold text-emphasis"
|
||||
>Access Key ID (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>If not provided, the default AWS credential chain is used (env vars, instance profile,
|
||||
EKS pod identity)</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'aws_sm_access_key_id',
|
||||
placeholder: 'AKIA...',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].access_key_id}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="aws_sm_secret_access_key" class="block text-xs font-semibold text-emphasis">Secret Access Key (optional)</label>
|
||||
<label for="aws_sm_secret_access_key" class="block text-xs font-semibold text-emphasis"
|
||||
>Secret Access Key (optional)</label
|
||||
>
|
||||
<Password bind:password={$values['secret_backend'].secret_access_key} small {disabled} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_prefix" class="block text-xs font-semibold text-emphasis">Secret Name Prefix (optional)</label>
|
||||
<span class="text-2xs text-secondary">Prefix for secret names in AWS Secrets Manager (default: windmill/)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_prefix', placeholder: 'windmill/', disabled }} bind:value={$values['secret_backend'].prefix} />
|
||||
<label for="aws_sm_prefix" class="block text-xs font-semibold text-emphasis"
|
||||
>Secret Name Prefix (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Prefix for secret names in AWS Secrets Manager (default: windmill/)</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{ type: 'text', id: 'aws_sm_prefix', placeholder: 'windmill/', disabled }}
|
||||
bind:value={$values['secret_backend'].prefix}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_endpoint_url" class="block text-xs font-semibold text-emphasis">Endpoint URL (optional)</label>
|
||||
<span class="text-2xs text-secondary">Custom endpoint for LocalStack or other compatible services</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_endpoint_url', placeholder: 'http://localhost:4566', disabled }} bind:value={$values['secret_backend'].endpoint_url} />
|
||||
<label for="aws_sm_endpoint_url" class="block text-xs font-semibold text-emphasis"
|
||||
>Endpoint URL (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Custom endpoint for LocalStack or other compatible services</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'aws_sm_endpoint_url',
|
||||
placeholder: 'http://localhost:4566',
|
||||
disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].endpoint_url}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAwsSmConnection} disabled={disabled || !isAwsSmConfigValid() || testingAwsSmConnection} loading={testingAwsSmConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testAwsSmConnection}
|
||||
disabled={disabled || !isAwsSmConfigValid() || testingAwsSmConnection}
|
||||
loading={testingAwsSmConnection}
|
||||
startIcon={{ icon: Server }}>Test Connection</Button
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Original values are NOT deleted to allow for rollback.</span
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">Database → AWS Secrets Manager</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingToAwsSm} startIcon={{ icon: ArrowRight }}>Migrate to AWS SM</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateToAwsSmModalOpen = true)}
|
||||
disabled={disabled || !isAwsSmConfigValid() || migratingToAwsSm}
|
||||
startIcon={{ icon: ArrowRight }}>Migrate to AWS SM</Button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<div class="flex items-center gap-2 mb-2"
|
||||
><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div
|
||||
>
|
||||
<p class="text-xs font-medium mb-2">AWS Secrets Manager → Database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingFromAwsSm} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
onclick={() => (migrateFromAwsSmModalOpen = true)}
|
||||
disabled={disabled || !isAwsSmConfigValid() || migratingFromAwsSm}
|
||||
startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,21 +859,105 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ConfirmationModal title="Migrate to AWS Secrets Manager" confirmationText="Migrate" open={migrateToAwsSmModalOpen} loading={migratingToAwsSm} type="reload" onCanceled={() => { migrateToAwsSmModalOpen = false }} onConfirmed={migrateSecretsToAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from the database to AWS Secrets Manager.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to AWS Secrets Manager"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToAwsSmModalOpen}
|
||||
loading={migratingToAwsSm}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToAwsSmModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToAwsSm}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets from the database to AWS Secrets Manager.</p><p
|
||||
class="text-yellow-600 dark:text-yellow-400 text-sm"
|
||||
>Database values are NOT deleted automatically.</p
|
||||
></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAwsSmModalOpen} loading={migratingFromAwsSm} type="reload" onCanceled={() => { migrateFromAwsSmModalOpen = false }} onConfirmed={migrateSecretsFromAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from AWS Secrets Manager back to the database.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">AWS Secrets Manager values are NOT deleted automatically.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateFromAwsSmModalOpen}
|
||||
loading={migratingFromAwsSm}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateFromAwsSmModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsFromAwsSm}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets from AWS Secrets Manager back to the database.</p><p
|
||||
class="text-yellow-600 dark:text-yellow-400 text-sm"
|
||||
>AWS Secrets Manager values are NOT deleted automatically.</p
|
||||
></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Azure Key Vault" confirmationText="Migrate" open={migrateToAzureKvModalOpen} loading={migratingToAzureKv} type="reload" onCanceled={() => { migrateToAzureKvModalOpen = false }} onConfirmed={migrateSecretsToAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to Azure Key Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to Azure Key Vault"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToAzureKvModalOpen}
|
||||
loading={migratingToAzureKv}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToAzureKvModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToAzureKv}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets to Azure Key Vault.</p><p
|
||||
class="text-yellow-600 dark:text-yellow-400 text-sm"
|
||||
>Database values are NOT deleted automatically.</p
|
||||
></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAzureKvModalOpen} loading={migratingFromAzureKv} type="reload" onCanceled={() => { migrateFromAzureKvModalOpen = false }} onConfirmed={migrateSecretsFromAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Azure Key Vault back to the database.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateFromAzureKvModalOpen}
|
||||
loading={migratingFromAzureKv}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateFromAzureKvModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsFromAzureKv}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets from Azure Key Vault back to the database.</p></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Vault" confirmationText="Migrate" open={migrateToVaultModalOpen} loading={migratingToVault} type="reload" onCanceled={() => { migrateToVaultModalOpen = false }} onConfirmed={migrateSecretsToVault}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to HashiCorp Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to Vault"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToVaultModalOpen}
|
||||
loading={migratingToVault}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToVaultModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToVault}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets to HashiCorp Vault.</p><p
|
||||
class="text-yellow-600 dark:text-yellow-400 text-sm"
|
||||
>Database values are NOT deleted automatically.</p
|
||||
></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateToDatabaseModalOpen} loading={migratingToDatabase} type="reload" onCanceled={() => { migrateToDatabaseModalOpen = false }} onConfirmed={migrateSecretsToDatabase}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Vault back to the database.</p></div>{/snippet}
|
||||
<ConfirmationModal
|
||||
title="Migrate to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToDatabaseModalOpen}
|
||||
loading={migratingToDatabase}
|
||||
type="reload"
|
||||
onCanceled={() => {
|
||||
migrateToDatabaseModalOpen = false
|
||||
}}
|
||||
onConfirmed={migrateSecretsToDatabase}
|
||||
>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"
|
||||
><p>This will copy all secrets from Vault back to the database.</p></div
|
||||
>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
Reference in New Issue
Block a user