Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/console/src/routeTree.gen.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Navigate, createFileRoute, useParams } from '@tanstack/react-router'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import {
PLATFORM_CONFIGURATION_FEATURE_FLAG,
PlatformConfiguration,
toPlatformCloudVendor,
toPlatformClusterMode,
useCluster,
usePlatformBinding,
} from '@qovery/domains/clusters/feature'
import { SettingsHeading } from '@qovery/shared/console-shared'
import { Callout, Icon, Section } from '@qovery/shared/ui'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/cluster/$clusterId/settings/platform'
)({
component: RouteComponent,
})

function RouteComponent() {
useDocumentTitle('Platform configuration - Cluster settings')

const { organizationId = '', clusterId = '' } = useParams({ strict: false })
const isPlatformConfigurationEnabled = useFeatureFlagEnabled(PLATFORM_CONFIGURATION_FEATURE_FLAG)
const { data: cluster, isLoading: isClusterLoading } = useCluster({ organizationId, clusterId })
const {
data: platformBinding,
isLoading: isPlatformBindingLoading,
isError: isPlatformBindingError,
} = usePlatformBinding({
organizationId,
clusterId,
enabled: Boolean(isPlatformConfigurationEnabled),
})
const clusterMode = toPlatformClusterMode(cluster?.kubernetes)
const cloudProvider = toPlatformCloudVendor(cluster?.cloud_provider)

if (isPlatformConfigurationEnabled === undefined) return null

// A binding fetch error is not "no binding": surface it instead of silently
// redirecting the user off the page.
if (isPlatformBindingError) {
return (
<Section className="p-8">
<SettingsHeading
title="Platform configuration"
description="Choose platform layers and configure their components. Changes are applied on the next cluster deployment."
/>
<Callout.Root color="red" className="max-w-content-with-navigation-left">
<Callout.Icon>
<Icon iconName="circle-exclamation" iconStyle="regular" />
</Callout.Icon>
<Callout.Text>The platform configuration could not be loaded. Refresh the page to try again.</Callout.Text>
</Callout.Root>
</Section>
)
}

if (
isPlatformConfigurationEnabled === false ||
(!isPlatformBindingLoading && !platformBinding) ||
(!isClusterLoading && (!clusterMode || !cloudProvider))
) {
return (
<Navigate
to="/organization/$organizationId/cluster/$clusterId/settings/general"
params={{ organizationId, clusterId }}
replace
/>
)
}

if (isPlatformBindingLoading || isClusterLoading || !clusterMode || !cloudProvider) return null

return (
<Section className="p-8">
<SettingsHeading
title="Platform configuration"
description="Choose platform layers and configure their components. Changes are applied on the next cluster deployment."
/>
<div className="max-w-content-with-navigation-left">
<PlatformConfiguration
key={clusterId}
organizationId={organizationId}
clusterId={clusterId}
clusterMode={clusterMode}
cloudProvider={cloudProvider}
/>
</div>
</Section>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Outlet, createFileRoute, useParams } from '@tanstack/react-router'
import clsx from 'clsx'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { match } from 'ts-pattern'
import { useCluster } from '@qovery/domains/clusters/feature'
import { PLATFORM_CONFIGURATION_FEATURE_FLAG, useCluster, usePlatformBinding } from '@qovery/domains/clusters/feature'
import { Sidebar } from '@qovery/shared/ui'
import { twMerge } from '@qovery/shared/util-js'

Expand All @@ -14,6 +14,12 @@ function RouteComponent() {
const { organizationId = '', clusterId = '' } = useParams({ strict: false })
const { data: cluster } = useCluster({ organizationId, clusterId })
const isEksAnywhereEnabled = useFeatureFlagEnabled('eks-anywhere')
const isPlatformConfigurationEnabled = useFeatureFlagEnabled(PLATFORM_CONFIGURATION_FEATURE_FLAG)
const { data: platformBinding } = usePlatformBinding({
organizationId,
clusterId,
enabled: Boolean(isPlatformConfigurationEnabled),
})

const pathSettings = `/organization/${organizationId}/cluster/${clusterId}/settings`

Expand Down Expand Up @@ -71,20 +77,30 @@ function RouteComponent() {
icon: 'gears' as const,
}

const platformConfigurationLink = {
title: 'Platform configuration',
to: `${pathSettings}/platform`,
icon: 'layer-group' as const,
}

const dangerZoneLink = {
title: 'Danger zone',
to: `${pathSettings}/danger-zone`,
icon: 'skull' as const,
}

const eksAnywhereCluster = isEksAnywhereEnabled && cluster?.kubernetes === 'PARTIALLY_MANAGED'
const hasPlatformTemplateBinding = Boolean(platformBinding?.templateKey && platformBinding.templateVersion)
const platformConfigurationLinks =
isPlatformConfigurationEnabled && hasPlatformTemplateBinding ? [platformConfigurationLink] : []

const LINKS_SETTINGS = match(cluster)
.with({ kubernetes: 'SELF_MANAGED' }, () => [
generalLink,
dnsProviderLink,
imageRegistryLink,
advancedSettingsLink,
...platformConfigurationLinks,
dangerZoneLink,
])
.with(
Expand All @@ -101,6 +117,7 @@ function RouteComponent() {
dnsProviderLink,
addonsLink,
...(eksAnywhereCluster ? [] : [advancedSettingsLink]),
...platformConfigurationLinks,
dangerZoneLink,
]
}
Expand All @@ -113,6 +130,7 @@ function RouteComponent() {
networkLink,
dnsProviderLink,
advancedSettingsLink,
...platformConfigurationLinks,
dangerZoneLink,
])
.with({ cloud_provider: 'GCP' }, () => [
Expand All @@ -123,6 +141,7 @@ function RouteComponent() {
dnsProviderLink,
addonsLink,
advancedSettingsLink,
...platformConfigurationLinks,
dangerZoneLink,
])
.with({ cloud_provider: 'AZURE' }, () => [
Expand All @@ -133,6 +152,7 @@ function RouteComponent() {
networkLink,
dnsProviderLink,
advancedSettingsLink,
...platformConfigurationLinks,
dangerZoneLink,
])
.otherwise(() => [])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { match } from 'ts-pattern'
import { StepGeneral } from '@qovery/domains/clusters/feature'
import { StepGeneral, useClusterContainerCreateContext } from '@qovery/domains/clusters/feature'
import { LabelSetting } from '@qovery/domains/organizations/feature'
import { type ClusterGeneralData } from '@qovery/shared/interfaces'
import { useDocumentTitle } from '@qovery/shared/util-hooks'
Expand All @@ -13,12 +13,15 @@ function General() {
useDocumentTitle('General - Create Cluster')
const { organizationId = '', slug } = useParams({ strict: false })
const navigate = useNavigate()
const { isEngineV2SelfManaged } = useClusterContainerCreateContext()

const creationFlowUrl = `/organization/${organizationId}/cluster/create/${slug}`

const handleSubmit = (data: ClusterGeneralData) => {
match(data)
.with({ installation_type: 'SELF_MANAGED' }, () => navigate({ to: `${creationFlowUrl}/kubeconfig` }))
.with({ installation_type: 'SELF_MANAGED' }, () =>
navigate({ to: `${creationFlowUrl}/${isEngineV2SelfManaged ? 'platform' : 'kubeconfig'}` })
)
.with({ installation_type: 'MANAGED', cloud_provider: 'GCP' }, () =>
navigate({ to: `${creationFlowUrl}/features` })
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Navigate, createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import {
PLATFORM_CONFIGURATION_FEATURE_FLAG,
StepPlatform,
useClusterContainerCreateContext,
} from '@qovery/domains/clusters/feature'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute('/_authenticated/organization/$organizationId/cluster/create/$slug/platform')({
component: Platform,
})

function Platform() {
useDocumentTitle('Platform layers - Create Cluster')
const { organizationId = '', slug = '' } = useParams({ strict: false })
const navigate = useNavigate()
const { isEngineV2SelfManaged } = useClusterContainerCreateContext()
const isPlatformConfigurationEnabled = useFeatureFlagEnabled(PLATFORM_CONFIGURATION_FEATURE_FLAG)
const creationFlowUrl = `/organization/${organizationId}/cluster/create/${slug}`

// Wait for the flag to resolve: it is undefined on first render, and redirecting
// then would bounce flag-enabled users off the step on every refresh/deep-link.
if (isPlatformConfigurationEnabled === undefined) return null

if (!isEngineV2SelfManaged) {
return (
<Navigate
to="/organization/$organizationId/cluster/create/$slug/general"
params={{ organizationId, slug }}
replace
/>
)
}

return (
<StepPlatform
organizationId={organizationId}
onPrevious={() => navigate({ to: `${creationFlowUrl}/general` })}
onSubmit={() => navigate({ to: `${creationFlowUrl}/summary` })}
/>
)
}
2 changes: 2 additions & 0 deletions libs/domains/clusters/data-access/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './lib/domains-clusters-data-access'
export * from './lib/cluster-operator/cluster-operator'
export * from './lib/platform-configuration/platform-configuration'
export { isGcpCluster } from './lib/cluster-checks/is-gcp-cluster'
export { isAwsCluster } from './lib/cluster-checks/is-aws-cluster'
export { excludeSecretManagerAccess } from './lib/secret-manager/exclude-secret-manager-access'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import axios from 'axios'
import { clusterOperator } from './cluster-operator'

describe('clusterOperator', () => {
afterEach(() => {
jest.restoreAllMocks()
})

it('maps the bootstrap response from the API snake case contract', async () => {
jest.spyOn(axios, 'get').mockResolvedValue({
data: {
chart_repository: 'https://helm.qovery.com',
chart_name: 'qovery-operator',
chart_version: '1.2.3',
namespace: 'qovery',
release_name: 'qovery-operator',
values: { clusterId: 'cluster-123' },
values_yaml: 'clusterId: cluster-123',
helm_command: 'helm upgrade --install qovery-operator',
},
})

const query = clusterOperator.bootstrap({ organizationId: 'org-123', clusterId: 'cluster-123' })
const result = await query.queryFn({} as Parameters<typeof query.queryFn>[0])

expect(result).toEqual({
chartRepository: 'https://helm.qovery.com',
chartName: 'qovery-operator',
chartVersion: '1.2.3',
namespace: 'qovery',
releaseName: 'qovery-operator',
values: { clusterId: 'cluster-123' },
valuesYaml: 'clusterId: cluster-123',
helmCommand: 'helm upgrade --install qovery-operator',
})
})

it('maps the operator status response from the API snake case contract', async () => {
jest.spyOn(axios, 'get').mockResolvedValue({
data: {
organization_id: 'org-123',
cluster_id: 'cluster-123',
operator_connected: true,
last_heartbeat: '2026-07-15T15:00:00Z',
operator_version: '1.2.3',
controller_version: '1.2.3',
request_schema_version: '1',
},
})

const query = clusterOperator.status({ organizationId: 'org-123', clusterId: 'cluster-123' })
const result = await query.queryFn({} as Parameters<typeof query.queryFn>[0])

expect(result).toEqual({
organizationId: 'org-123',
clusterId: 'cluster-123',
operatorConnected: true,
lastHeartbeat: '2026-07-15T15:00:00Z',
operatorVersion: '1.2.3',
controllerVersion: '1.2.3',
requestSchemaVersion: '1',
})
})
})
Loading
Loading