mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
* fix(datatable): provision the replication user on managed postgres * fix(datatable): serialize replication user provisioning and sync config schema * fix(datatable): keep replication cleanup best-effort and self-heal a null password
267 lines
10 KiB
Svelte
267 lines
10 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
SettingService,
|
|
type CustomInstanceDb,
|
|
type CustomInstanceDbTag,
|
|
type ListCustomInstanceDbsResponse
|
|
} from '$lib/gen'
|
|
import { slide } from 'svelte/transition'
|
|
import Modal2 from '../common/modal/Modal2.svelte'
|
|
import Alert from '../common/alert/Alert.svelte'
|
|
import LoggedWizardResult, { firstEmptyStepIsError } from '../wizards/LoggedWizardResult.svelte'
|
|
import Button from '../common/button/Button.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import { isCustomInstanceDbEnabled } from './utils.svelte'
|
|
import type { ResourceReturn } from 'runed'
|
|
import type { ConfirmationModalHandle } from '../common/confirmationModal/asyncConfirmationModal.svelte'
|
|
import ExploreAssetButton from '../ExploreAssetButton.svelte'
|
|
import { ArrowRight, InfoIcon, Trash2 } from 'lucide-svelte'
|
|
import type { Snippet } from 'svelte'
|
|
import { truncate } from '$lib/utils'
|
|
import Tooltip from '../meltComponents/Tooltip.svelte'
|
|
import { superadmin } from '$lib/stores'
|
|
|
|
type Props = {
|
|
customInstanceDbs: ResourceReturn<ListCustomInstanceDbsResponse>
|
|
confirmationModal: ConfirmationModalHandle
|
|
bottomHint?: Snippet | undefined
|
|
opened: { status: CustomInstanceDb | undefined; dbname: string } | undefined
|
|
tag?: CustomInstanceDbTag
|
|
}
|
|
|
|
let {
|
|
customInstanceDbs,
|
|
confirmationModal,
|
|
bottomHint,
|
|
opened = $bindable(),
|
|
tag
|
|
}: Props = $props()
|
|
|
|
let customInstanceDbSetupIsRunning = $state(false)
|
|
let dropIsRunning = $state(false)
|
|
let preventClose = false
|
|
</script>
|
|
|
|
<Modal2
|
|
bind:isOpen={() => !!opened, (v) => !v && !preventClose && (opened = undefined)}
|
|
target="#content"
|
|
title={'Custom Instance Database Setup'}
|
|
contentClasses="flex flex-col"
|
|
fixedWidth="md"
|
|
fixedHeight="md"
|
|
>
|
|
{#if opened}
|
|
{@const status = opened?.status}
|
|
{@const dbname = opened?.dbname}
|
|
{@const enableManageButton =
|
|
status?.logs.created_database === 'OK' || status?.logs.created_database === 'SKIP'}
|
|
<div class="flex h-full divide-x gap-4">
|
|
<div class="basis-2/5 grow-0 shrink-0 flex flex-col">
|
|
<div class="flex-1 flex flex-col">
|
|
<span class="text-sm font-bold mb-2 overflow break-all">{dbname}</span>
|
|
<span class="text-sm">
|
|
Custom instance databases are databases created in the Windmill PostgreSQL instance.
|
|
Their credentials are automatically managed by Windmill and are never exposed to users.
|
|
Only super admins can create them.
|
|
</span>
|
|
</div>
|
|
<div class="pt-6">
|
|
{#if bottomHint}
|
|
<div class="text-secondary text-2xs mb-2">
|
|
{@render bottomHint()}
|
|
</div>
|
|
{/if}
|
|
<div class="flex gap-2">
|
|
<ExploreAssetButton
|
|
class="flex-1"
|
|
asset={{ kind: 'resource', path: 'CUSTOM_INSTANCE_DB/' + dbname }}
|
|
_resourceMetadata={{ resource_type: 'postgresql' }}
|
|
disabled={!$isCustomInstanceDbEnabled || !enableManageButton}
|
|
onClick={() => (opened = undefined)}
|
|
/>
|
|
{#if $superadmin}
|
|
<Button
|
|
size="sm"
|
|
destructive
|
|
iconOnly
|
|
variant="accent"
|
|
startIcon={{ icon: Trash2 }}
|
|
loading={dropIsRunning}
|
|
onClick={async () => {
|
|
preventClose = true
|
|
let confirm = await confirmationModal.ask({
|
|
title: 'Drop database',
|
|
children: `This will permanently drop the database "${dbname}". All data will be lost. This action is irreversible.`,
|
|
confirmationText: 'Drop database'
|
|
})
|
|
preventClose = false
|
|
if (!confirm) return
|
|
|
|
try {
|
|
dropIsRunning = true
|
|
await SettingService.dropCustomInstanceDb({ name: dbname })
|
|
await customInstanceDbs.refetch()
|
|
sendUserToast(`Database "${dbname}" dropped successfully`)
|
|
opened = undefined
|
|
} catch (e) {
|
|
sendUserToast(`Failed to drop database: ${e}`, true)
|
|
console.error('Error dropping custom instance database', e)
|
|
} finally {
|
|
dropIsRunning = false
|
|
}
|
|
}}
|
|
>
|
|
Drop database
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 shrink-0 flex flex-col pl-4 gap-2">
|
|
<div class="flex-1 overflow-y-scroll">
|
|
{#if status?.error}
|
|
<div transition:slide={{ duration: 200 }} class="mb-4">
|
|
<Alert title="Error setting up custom instance database" type="error">
|
|
{status.error}
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
|
|
<LoggedWizardResult
|
|
steps={firstEmptyStepIsError(
|
|
[
|
|
{
|
|
title: 'Super admin required',
|
|
status: status?.logs.super_admin,
|
|
description:
|
|
'You need to be a super admin to create a new database in the Windmill PostgreSQL instance'
|
|
},
|
|
{
|
|
title: 'Retrieve and parse database credentials',
|
|
status: status?.logs.database_credentials,
|
|
description:
|
|
'Windmill uses the DATABASE_URL or DATABASE_URL_FILE environment variable to connect to the PostgreSQL instance. Make sure it is correctly set'
|
|
},
|
|
{
|
|
title: 'Database name is valid',
|
|
status: status?.logs.valid_dbname,
|
|
description:
|
|
'The database name must be alphanumeric (underscores and hyphens allowed) and cannot be named the same as the Windmill database (usually "windmill")'
|
|
},
|
|
{
|
|
title:
|
|
'Create database' +
|
|
(status?.logs.created_database === 'SKIP' ? ' (already exists, skipped)' : ''),
|
|
status: status?.logs.created_database,
|
|
description: `In the Windmill PostgreSQL instance, run: CREATE DATABASE "${dbname}".`
|
|
},
|
|
{
|
|
title: `Connect to the ${dbname} database`,
|
|
status: status?.logs.db_connect,
|
|
description:
|
|
"Connect to the newly created database with the default admin user (the one in DATABASE_URL, usually 'postgres') to run the next commands"
|
|
},
|
|
{
|
|
title: 'Grant permissions to custom_instance_user',
|
|
status: status?.logs.grant_permissions,
|
|
description:
|
|
'Gives custom_instance_user the required permissions to use the database. custom_instance_user is already created during a migration and has an auto-generated password stored in global_settings.custom_instance_pg_databases.user_pwd. These are the commands : \n\n' +
|
|
`GRANT CONNECT ON DATABASE "${dbname}" TO custom_instance_user;\n` +
|
|
'GRANT USAGE ON SCHEMA public TO custom_instance_user;\n' +
|
|
'GRANT CREATE ON SCHEMA public TO custom_instance_user;\n' +
|
|
`GRANT CREATE ON DATABASE "${dbname}" TO custom_instance_user;\n` +
|
|
'ALTER DEFAULT PRIVILEGES IN SCHEMA public \n' +
|
|
' GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES\n TO custom_instance_user;\n' +
|
|
'ALTER ROLE custom_instance_user CREATEROLE;'
|
|
},
|
|
{
|
|
title: 'Grant replication to custom_instance_replication_user',
|
|
status: status?.logs.replication_user,
|
|
description:
|
|
'Postgres triggers on custom-instance datatables connect as custom_instance_replication_user, whose password is stored in global_settings.custom_instance_replication_pwd. The role is cluster-wide, so it is created on the Windmill PostgreSQL instance rather than on this database : \n\n' +
|
|
'ALTER ROLE custom_instance_replication_user REPLICATION;\n' +
|
|
'GRANT custom_instance_user TO custom_instance_replication_user;\n\n' +
|
|
'Setting REPLICATION requires a superuser on PostgreSQL 15 and older. Managed instances never grant one, so on AWS RDS Windmill falls back to GRANT rds_replication TO custom_instance_replication_user. The database stays usable for datatables if this step fails, but postgres triggers on them do not.' +
|
|
(status?.logs.replication_user_error
|
|
? `\n\nError: ${status.logs.replication_user_error}`
|
|
: '')
|
|
}
|
|
],
|
|
status?.error ?? undefined
|
|
)}
|
|
/>
|
|
</div>
|
|
{#if $superadmin}
|
|
<Tooltip>
|
|
<Button
|
|
endIcon={{ icon: InfoIcon }}
|
|
onClick={async () => {
|
|
await SettingService.refreshCustomInstanceUserPwd()
|
|
sendUserToast('custom instance user passwords refreshed')
|
|
}}>Refresh custom instance passwords</Button
|
|
>
|
|
{#snippet text()}
|
|
Try this if there is an issue with your custom instance database passwords. Rotates
|
|
both custom_instance_user and the custom_instance_replication_user used by postgres
|
|
triggers.
|
|
{/snippet}
|
|
</Tooltip>
|
|
{/if}
|
|
<Button
|
|
size="sm"
|
|
id="run-custom-instance-db-setup-button"
|
|
variant={!status?.success ? 'accent' : 'default'}
|
|
endIcon={status?.success ? undefined : { icon: ArrowRight }}
|
|
disabled={!$isCustomInstanceDbEnabled}
|
|
onClick={async () => {
|
|
if (customInstanceDbSetupIsRunning) return
|
|
|
|
let wasAlreadySuccessful = status?.success ?? false
|
|
if (status?.logs.created_database != 'OK' && status?.logs.created_database != 'SKIP') {
|
|
preventClose = true
|
|
let confirm = await confirmationModal.ask({
|
|
title: 'Confirm setup',
|
|
children: `This will create a new database ${dbname} in the Windmill PostgreSQL instance`,
|
|
confirmationText: 'Setup database'
|
|
})
|
|
preventClose = false
|
|
if (!confirm) return
|
|
}
|
|
|
|
try {
|
|
customInstanceDbSetupIsRunning = true
|
|
let result = await SettingService.setupCustomInstanceDb({
|
|
name: dbname,
|
|
requestBody: { tag }
|
|
})
|
|
await customInstanceDbs.refetch()
|
|
if (result.success) {
|
|
if (!wasAlreadySuccessful) sendUserToast('Setup successful')
|
|
else sendUserToast('Check successful')
|
|
} else {
|
|
sendUserToast(result.error ?? 'An error occurred', true)
|
|
}
|
|
} catch (e) {
|
|
sendUserToast('Unexpected error, check console for details', true)
|
|
console.error('Error setting up custom instance database', e)
|
|
} finally {
|
|
customInstanceDbSetupIsRunning = false
|
|
}
|
|
}}
|
|
loading={customInstanceDbSetupIsRunning}
|
|
>
|
|
{#if !$isCustomInstanceDbEnabled}
|
|
Only superadmins can setup custom instance databases
|
|
{:else if status?.success}
|
|
Check again
|
|
{:else if status?.error}
|
|
Try again
|
|
{:else}
|
|
Setup {truncate(dbname, 24)}
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Modal2>
|