Skip to content
Draft
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,99 @@
import { Navigate, createFileRoute, useParams } from '@tanstack/react-router'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import {
ClusterOperatorStatus,
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">
{clusterMode === 'CUSTOMER_MANAGED' ? (
<div className="mb-6">
<ClusterOperatorStatus organizationId={organizationId} clusterId={clusterId} />
</div>
) : null}
<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,82 @@
import { ClusterOperatorApi } from 'qovery-typescript-axios'
import { clusterOperator, clusterOperatorMutations } from './cluster-operator'

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

it('gets the operator bootstrap through the generated client', async () => {
const bootstrap = {
chart_repository: 'oci://public.ecr.aws/r3m4q3r9',
chart_name: 'qovery-operator',
chart_version: '0.2.1',
chart_reference: 'oci://public.ecr.aws/r3m4q3r9/qovery-operator',
namespace: 'qovery',
release_name: 'qovery-operator',
values: { clusterId: 'cluster-123' },
values_yaml: 'clusterId: cluster-123',
helm_command: 'helm upgrade --install qovery-operator',
}
const getBootstrap = jest
.spyOn(ClusterOperatorApi.prototype, 'getClusterOperatorBootstrap')
.mockResolvedValue({ data: bootstrap } as never)

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

it('gets the operator status through the generated client', async () => {
const status = {
organization_id: 'org-123',
cluster_id: 'cluster-123',
operator_connected: true,
last_heartbeat: '2026-07-15T15:00:00Z',
operator_version: 'v1.203.0',
desired_image_version: 'v1.203.0',
desired_chart_version: '0.2.1',
reported_chart_version: '0.2.1',
status: 'CURRENT' as const,
}
const getStatus = jest
.spyOn(ClusterOperatorApi.prototype, 'getClusterOperatorStatus')
.mockResolvedValue({ data: status } as never)

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

it('attaches the operator through the generated client', async () => {
const attach = jest.spyOn(ClusterOperatorApi.prototype, 'attachClusterOperator').mockResolvedValue({} as never)

await clusterOperatorMutations.attach({ organizationId: 'org-123', clusterId: 'cluster-123' })

expect(attach).toHaveBeenCalledWith('org-123', 'cluster-123')
})

it('updates the operator through the generated client', async () => {
const updateResponse = {
execution_id: 'execution-123',
image_version: 'v1.203.0',
chart_version: '0.2.1',
}
const update = jest
.spyOn(ClusterOperatorApi.prototype, 'updateClusterOperator')
.mockResolvedValue({ data: updateResponse } as never)

await expect(
clusterOperatorMutations.update({
organizationId: 'org-123',
clusterId: 'cluster-123',
imageVersion: 'v1.203.0',
chartVersion: '0.2.1',
})
).resolves.toEqual(updateResponse)
expect(update).toHaveBeenCalledWith('org-123', 'cluster-123', {
image_version: 'v1.203.0',
chart_version: '0.2.1',
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createQueryKeys } from '@lukemorales/query-key-factory'
import { ClusterOperatorApi } from 'qovery-typescript-axios'
import { isHttpStatus } from '../http/is-http-status'

const clusterOperatorApi = new ClusterOperatorApi()

export const clusterOperator = createQueryKeys('clusterOperator', {
status: ({ organizationId, clusterId }: { organizationId: string; clusterId: string }) => ({
queryKey: [organizationId, clusterId],
async queryFn() {
try {
const response = await clusterOperatorApi.getClusterOperatorStatus(organizationId, clusterId)
return response.data
} catch (error) {
if (isHttpStatus(error, 404)) return null
throw error
}
},
}),
bootstrap: ({ organizationId, clusterId }: { organizationId: string; clusterId: string }) => ({
queryKey: [organizationId, clusterId],
async queryFn() {
const response = await clusterOperatorApi.getClusterOperatorBootstrap(organizationId, clusterId)
return response.data
},
}),
})

export const clusterOperatorMutations = {
async attach({ organizationId, clusterId }: { organizationId: string; clusterId: string }) {
await clusterOperatorApi.attachClusterOperator(organizationId, clusterId)
},
async update({
organizationId,
clusterId,
chartVersion,
imageVersion,
}: {
organizationId: string
clusterId: string
chartVersion: string
imageVersion?: string | null
}) {
const response = await clusterOperatorApi.updateClusterOperator(organizationId, clusterId, {
chart_version: chartVersion,
image_version: imageVersion,
})
return response.data
},
}
Loading
Loading