mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Follow-up to #49131. For `AWS_NIMBUS` orgs, the new-project form's Region trigger could show a region that wasn't in the dropdown at all (e.g. "Southeast Asia (Singapore)" while the list only offered "East US (North Virginia)"). The geolocation-based default region (`useDefaultRegionQuery`) picked the nearest region from **all** AWS regions and seeded it into `dbRegion` unvalidated, ignoring the provider's restricted region list. **Changed:** - `getDefaultRegionOption` now computes the nearest region only over the provider's available regions (new `getDefaultRegionCandidateKeys` helper). The flag-based restricted pool (`defaultRegionRestrictedPool`) narrows within that set and is ignored if the intersection would be empty. - The form's default-region selection is extracted into `resolveDefaultDbRegion` (`ProjectCreation.utils.ts`): High Availability region first, then the recommended smart region, then the geolocated default — used only when the provider actually offers that region — falling back to the provider's static default. - `getAvailableRegions` takes an injectable `environment` param (same pattern as `getHighAvailabilityRegionCode`) so the prod-only Nimbus region list is unit-testable. **Added:** - Unit tests for `getDefaultRegionCandidateKeys` (provider clamping incl. Nimbus on prod, restricted-pool intersection, empty-intersection fallback), `getAvailableRegions` across environments, and `resolveDefaultDbRegion` (branch priority plus the fallback when the geolocated region isn't offered). ## To test - Emulate a Nimbus org locally by setting `"infra:cloud_providers": ["AWS_NIMBUS"]` in `apps/studio/hooks/custom-content/custom-content.json`, then open the new-project form: the Region trigger must show the same region the dropdown offers (locally that's only Southeast Asia (Singapore)). To reproduce the original mismatch path, stub `https://www.cloudflare.com/cdn-cgi/trace` to return `loc=US` — the trigger should still be clamped to the provider's region rather than showing a US region - Block or fail the Cloudflare trace request: the trigger should fall back to the provider's static default region, not sit blank or loading - Restore the normal provider list: the smart-region flow ("General regions" + "Specific regions" with Recommended badges) is unaffected — the geolocation request doesn't even fire on that path — and toggling High Availability still transitions the region list cleanly <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **Bug Fixes** - Region suggestions now respect the selected cloud provider and deployment environment. - Project creation avoids unavailable geolocated regions and falls back to a supported provider default. - Restricted region pools now fall back reliably to available provider regions. - AWS Nimbus selection reflects the active environment while preserving high-availability and smart-region behavior. - **Tests** - Added coverage for provider-specific, environment-specific, restricted, and fallback region selection scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { useFlag } from 'common'
|
|
import type { CloudProvider } from 'shared-data'
|
|
import { AWS_REGIONS } from 'shared-data'
|
|
|
|
import { miscKeys } from './keys'
|
|
import { COUNTRY_LAT_LON } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants'
|
|
import { getAvailableRegions } from '@/components/interfaces/ProjectCreation/ProjectCreation.utils'
|
|
import { AWS_REGIONS_COORDINATES } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
|
import { fetchHandler } from '@/data/fetchers'
|
|
import { getDistanceLatLonKM, tryParseJson } from '@/lib/helpers'
|
|
import type { ResponseError, UseCustomQueryOptions } from '@/types'
|
|
|
|
export type DefaultRegionVariables = {
|
|
cloudProvider?: CloudProvider
|
|
restrictedPool?: string[]
|
|
useRestrictedPool?: boolean
|
|
}
|
|
|
|
// The geolocation-based default may only ever pick a region the selected cloud provider
|
|
// actually offers (e.g. AWS_NIMBUS is restricted to a single region). The flag-based
|
|
// restricted pool narrows within that set, and is ignored if it would leave no candidates.
|
|
export function getDefaultRegionCandidateKeys(
|
|
cloudProvider: CloudProvider,
|
|
restrictedPool?: string[],
|
|
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
|
|
) {
|
|
const providerRegionKeys = Object.keys(getAvailableRegions(cloudProvider, environment))
|
|
const pooledRegionKeys = restrictedPool
|
|
? providerRegionKeys.filter((key) => restrictedPool.includes(key))
|
|
: providerRegionKeys
|
|
return pooledRegionKeys.length > 0 ? pooledRegionKeys : providerRegionKeys
|
|
}
|
|
|
|
export async function getDefaultRegionOption({
|
|
cloudProvider,
|
|
restrictedPool,
|
|
useRestrictedPool = true,
|
|
}: DefaultRegionVariables) {
|
|
if (!cloudProvider) throw new Error('Cloud provider is required')
|
|
|
|
try {
|
|
const data = await fetchHandler('https://www.cloudflare.com/cdn-cgi/trace').then((res) =>
|
|
res.text()
|
|
)
|
|
const locationCode: keyof typeof COUNTRY_LAT_LON = Object.fromEntries(
|
|
data.split('\n').map((item) => item.split('='))
|
|
)['loc']
|
|
const locLatLon = COUNTRY_LAT_LON[locationCode]
|
|
|
|
if (locLatLon === undefined) return undefined
|
|
|
|
const candidateKeys = getDefaultRegionCandidateKeys(
|
|
cloudProvider,
|
|
useRestrictedPool ? restrictedPool : undefined
|
|
)
|
|
const locations = Object.fromEntries(
|
|
Object.entries(AWS_REGIONS_COORDINATES).filter(([key]) => candidateKeys.includes(key))
|
|
)
|
|
|
|
const distances = Object.keys(locations).map((reg) => {
|
|
const region: { lat: number; lon: number } = {
|
|
lat: locations[reg][1],
|
|
lon: locations[reg][0],
|
|
}
|
|
return getDistanceLatLonKM(locLatLon.lat, locLatLon.lon, region.lat, region.lon)
|
|
})
|
|
const shortestDistance = Math.min(...distances)
|
|
const closestRegion = Object.keys(locations)[distances.indexOf(shortestDistance)]
|
|
|
|
return AWS_REGIONS[closestRegion as keyof typeof AWS_REGIONS].displayName
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export type DefaultRegionData = Awaited<ReturnType<typeof getDefaultRegionOption>>
|
|
export type DefaultRegionError = ResponseError
|
|
|
|
export const useDefaultRegionQuery = <TData = DefaultRegionData>(
|
|
{ cloudProvider, useRestrictedPool }: DefaultRegionVariables,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: UseCustomQueryOptions<DefaultRegionData, DefaultRegionError, TData> = {}
|
|
) => {
|
|
// [Joshen] Flag allows us to specify restricted regions for users based on percentage
|
|
const restrictedPoolFlag = useFlag('defaultRegionRestrictedPool')
|
|
const restrictedPool = tryParseJson(restrictedPoolFlag)
|
|
|
|
return useQuery<DefaultRegionData, DefaultRegionError, TData>({
|
|
queryKey: miscKeys.defaultRegion(cloudProvider, useRestrictedPool ?? true),
|
|
queryFn: () => getDefaultRegionOption({ cloudProvider, restrictedPool, useRestrictedPool }),
|
|
enabled:
|
|
enabled && typeof cloudProvider !== 'undefined' && typeof restrictedPool !== 'undefined',
|
|
retry(failureCount) {
|
|
return failureCount < 1
|
|
},
|
|
...options,
|
|
})
|
|
}
|