diff --git a/packages/manager/.changeset/pr-11415-upcoming-features-1734109279111.md b/packages/manager/.changeset/pr-11415-upcoming-features-1734109279111.md new file mode 100644 index 00000000000..0db09e80bd4 --- /dev/null +++ b/packages/manager/.changeset/pr-11415-upcoming-features-1734109279111.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Kubernetes version upgrade components for LKE-E ([#11415](https://github.com/linode/manager/pull/11415)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 4d4f89c60cb..8242aca2d72 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -29,6 +29,7 @@ import { mockGetControlPlaneACL, mockUpdateControlPlaneACLError, mockGetControlPlaneACLError, + mockGetTieredKubernetesVersions, } from 'support/intercepts/lke'; import { mockGetLinodeType, @@ -133,7 +134,7 @@ describe('LKE cluster updates', () => { * - Confirms that Kubernetes upgrade prompt is shown when not up-to-date. * - Confirms that Kubernetes upgrade prompt is hidden when up-to-date. */ - it('can upgrade kubernetes version from the details page', () => { + it('can upgrade standard kubernetes version from the details page', () => { const oldVersion = '1.25'; const newVersion = '1.26'; @@ -235,7 +236,7 @@ describe('LKE cluster updates', () => { ui.toast.findByMessage('Recycle started successfully.'); }); - it('can upgrade the kubernetes version from the landing page', () => { + it('can upgrade the standard kubernetes version from the landing page', () => { const oldVersion = '1.25'; const newVersion = '1.26'; @@ -294,6 +295,208 @@ describe('LKE cluster updates', () => { cy.findByText(newVersion).should('be.visible'); }); + /* + * - Confirms UI flow of upgrading Kubernetes enterprise version using mocked API requests. + * - Confirms that Kubernetes upgrade prompt is shown when not up-to-date. + * - Confirms that Kubernetes upgrade prompt is hidden when up-to-date. + */ + it('can upgrade enterprise kubernetes version from the details page', () => { + const oldVersion = '1.31.1+lke1'; + const newVersion = '1.31.1+lke2'; + + mockGetAccount( + accountFactory.build({ + capabilities: ['Kubernetes Enterprise'], + }) + ).as('getAccount'); + + // TODO LKE-E: Remove once feature is in GA + mockAppendFeatureFlags({ + lkeEnterprise: { enabled: true, la: true }, + }); + + const mockCluster = kubernetesClusterFactory.build({ + k8s_version: oldVersion, + tier: 'enterprise', + }); + + const mockClusterUpdated = { + ...mockCluster, + k8s_version: newVersion, + }; + + const upgradePrompt = + 'A new version of Kubernetes is available (1.31.1+lke2).'; + + const upgradeNotes = [ + 'Once the upgrade is complete you will need to recycle all nodes in your cluster', + // Confirm that the old version and new version are both shown. + oldVersion, + newVersion, + ]; + + mockGetCluster(mockCluster).as('getCluster'); + mockGetTieredKubernetesVersions('enterprise', [ + { id: newVersion, tier: 'enterprise' }, + { id: oldVersion, tier: 'enterprise' }, + ]).as('getTieredVersions'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + mockUpdateCluster(mockCluster.id, mockClusterUpdated).as('updateCluster'); + mockGetDashboardUrl(mockCluster.id); + mockGetApiEndpoints(mockCluster.id); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); + cy.wait([ + '@getAccount', + '@getCluster', + '@getNodePools', + '@getTieredVersions', + ]); + + // Confirm that upgrade prompt is shown. + cy.findByText(upgradePrompt).should('be.visible'); + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle( + `Step 1: Upgrade ${mockCluster.label} to Kubernetes ${newVersion}` + ) + .should('be.visible') + .within(() => { + upgradeNotes.forEach((note: string) => { + cy.findAllByText(note, { exact: false }).should('be.visible'); + }); + + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Wait for API response and assert toast message is shown. + cy.wait('@updateCluster'); + + // Verify the banner goes away because the version update has happened + cy.findByText(upgradePrompt).should('not.exist'); + + mockRecycleAllNodes(mockCluster.id).as('recycleAllNodes'); + + const stepTwoDialogTitle = 'Step 2: Recycle All Cluster Nodes'; + + ui.dialog + .findByTitle(stepTwoDialogTitle) + .should('be.visible') + .within(() => { + cy.findByText('Kubernetes version has been updated successfully.', { + exact: false, + }).should('be.visible'); + + cy.findByText( + 'For the changes to take full effect you must recycle the nodes in your cluster.', + { exact: false } + ).should('be.visible'); + + ui.button + .findByTitle('Recycle All Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Verify clicking the "Recycle All Nodes" makes an API call + cy.wait('@recycleAllNodes'); + + // Verify the upgrade dialog closed + cy.findByText(stepTwoDialogTitle).should('not.exist'); + + // Verify the banner is still gone after the flow + cy.findByText(upgradePrompt).should('not.exist'); + + // Verify the version is correct after the update + cy.findByText(`Version ${newVersion}`); + + ui.toast.findByMessage('Recycle started successfully.'); + }); + + it('can upgrade the enterprise kubernetes version from the landing page', () => { + const oldVersion = '1.31.1+lke1'; + const newVersion = '1.32.1+lke2'; + + mockGetAccount( + accountFactory.build({ + capabilities: ['Kubernetes Enterprise'], + }) + ).as('getAccount'); + + // TODO LKE-E: Remove once feature is in GA + mockAppendFeatureFlags({ + lkeEnterprise: { enabled: true, la: true }, + }); + + const cluster = kubernetesClusterFactory.build({ + k8s_version: oldVersion, + tier: 'enterprise', + }); + + const updatedCluster = { ...cluster, k8s_version: newVersion }; + + mockGetClusters([cluster]).as('getClusters'); + mockGetTieredKubernetesVersions('enterprise', [ + { id: newVersion, tier: 'enterprise' }, + { id: oldVersion, tier: 'enterprise' }, + ]).as('getTieredVersions'); + mockUpdateCluster(cluster.id, updatedCluster).as('updateCluster'); + mockRecycleAllNodes(cluster.id).as('recycleAllNodes'); + + cy.visitWithLogin(`/kubernetes/clusters`); + + cy.wait(['@getAccount', '@getClusters', '@getTieredVersions']); + + cy.findByText(oldVersion).should('be.visible'); + + cy.findByText('UPGRADE') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle( + `Step 1: Upgrade ${cluster.label} to Kubernetes ${newVersion}` + ) + .should('be.visible'); + + mockGetClusters([updatedCluster]).as('getClusters'); + + ui.button + .findByTitle('Upgrade Version') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait(['@updateCluster', '@getClusters']); + + ui.dialog + .findByTitle('Step 2: Recycle All Cluster Nodes') + .should('be.visible'); + + ui.button + .findByTitle('Recycle All Nodes') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@recycleAllNodes'); + + ui.toast.assertMessage('Recycle started successfully.'); + + cy.findByText(newVersion).should('be.visible'); + }); + /* * - Confirms node, node pool, and cluster recycling UI flow using mocked API data. * - Confirms that user is warned that recycling recreates nodes and may take a while. diff --git a/packages/manager/src/factories/kubernetesCluster.ts b/packages/manager/src/factories/kubernetesCluster.ts index 9680a530b95..fe8d0e24e7e 100644 --- a/packages/manager/src/factories/kubernetesCluster.ts +++ b/packages/manager/src/factories/kubernetesCluster.ts @@ -9,6 +9,7 @@ import type { KubernetesControlPlaneACLPayload, KubernetesDashboardResponse, KubernetesEndpointResponse, + KubernetesTieredVersion, KubernetesVersion, PoolNodeResponse, } from '@linode/api-v4/lib/kubernetes/types'; @@ -78,6 +79,20 @@ export const kubernetesVersionFactory = Factory.Sync.makeFactory( + { + id: Factory.each((id) => `'v1.3${id}'`), + tier: 'standard', + } +); + +export const kubernetesEnterpriseTierVersionFactory = Factory.Sync.makeFactory( + { + id: Factory.each((id) => `'v1.31.${id}+lke1'`), + tier: 'enterprise', + } +); + export const kubernetesControlPlaneACLOptionsFactory = Factory.Sync.makeFactory( { addresses: { diff --git a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx index 9678d59721f..eba12a51dac 100644 --- a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx +++ b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.tsx @@ -1,5 +1,4 @@ import { Chip } from '@linode/ui'; -import { KubeNodePoolResponse, KubernetesCluster } from '@linode/api-v4'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { Link } from 'react-router-dom'; @@ -9,10 +8,7 @@ import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { - useAllKubernetesNodePoolQuery, - useKubernetesVersionQuery, -} from 'src/queries/kubernetes'; +import { useAllKubernetesNodePoolQuery } from 'src/queries/kubernetes'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { useSpecificTypes } from 'src/queries/types'; import { extendTypesQueryResult } from 'src/utilities/extendType'; @@ -20,9 +16,12 @@ import { extendTypesQueryResult } from 'src/utilities/extendType'; import { getNextVersion, getTotalClusterMemoryCPUAndStorage, + useLkeStandardOrEnterpriseVersions, } from '../kubeUtils'; import { ClusterActionMenu } from './ClusterActionMenu'; +import type { KubeNodePoolResponse, KubernetesCluster } from '@linode/api-v4'; + const useStyles = makeStyles()(() => ({ clusterRow: { '&:before': { @@ -64,7 +63,6 @@ export const KubernetesClusterRow = (props: Props) => { const { cluster, openDeleteDialog, openUpgradeDialog } = props; const { classes } = useStyles(); - const { data: versions } = useKubernetesVersionQuery(); const { data: pools } = useAllKubernetesNodePoolQuery(cluster.id); const typesQuery = useSpecificTypes(pools?.map((pool) => pool.type) ?? []); const types = extendTypesQueryResult(typesQuery); @@ -72,6 +70,10 @@ export const KubernetesClusterRow = (props: Props) => { const region = regions?.find((r) => r.id === cluster.region); + const { versions } = useLkeStandardOrEnterpriseVersions( + cluster.tier ?? 'standard' //TODO LKE: remove fallback once LKE-E is in GA and tier is required + ); + const nextVersion = getNextVersion(cluster.k8s_version, versions ?? []); const hasUpgrade = nextVersion !== null; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index e9e3c3c2ad9..23073f5cb30 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -19,6 +19,7 @@ import { getLatestVersion, useAPLAvailability, useIsLkeEnterpriseEnabled, + useLkeStandardOrEnterpriseVersions, } from 'src/features/Kubernetes/kubeUtils'; import { useAccount } from 'src/queries/account/account'; import { @@ -28,9 +29,7 @@ import { import { useCreateKubernetesClusterBetaMutation, useCreateKubernetesClusterMutation, - useKubernetesTieredVersionsQuery, useKubernetesTypesQuery, - useKubernetesVersionQuery, } from 'src/queries/kubernetes'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { useAllTypes } from 'src/queries/types'; @@ -130,31 +129,16 @@ export const CreateCluster = () => { mutateAsync: createKubernetesClusterBeta, } = useCreateKubernetesClusterBetaMutation(); - const { - data: _versionData, - isError: versionLoadError, - isLoading: versionLoading, - } = useKubernetesVersionQuery(); - - const { - data: enterpriseTierVersionData, - isLoading: enterpriseTierVersionDataIsLoading, - } = useKubernetesTieredVersionsQuery('enterprise'); - const { isLkeEnterpriseLAFeatureEnabled, isLkeEnterpriseLAFlagEnabled, } = useIsLkeEnterpriseEnabled(); - /** - * If LKE-E is enabled, use the new /versions/ endpoint data, which supports enterprise tiers. - * If LKE-E is disabled, use the data from the existing /versions endpoint. - * @todo LKE-E: Clean up use of versionData once LKE-E is in GA. - */ - const versionData = - isLkeEnterpriseLAFeatureEnabled && selectedTier === 'enterprise' - ? enterpriseTierVersionData - : _versionData; + const { + isLoadingVersions, + versions: versionData, + versionsError, + } = useLkeStandardOrEnterpriseVersions(selectedTier); const versions = (versionData ?? []).map((thisVersion) => ({ label: thisVersion.id, @@ -303,7 +287,7 @@ export const CreateCluster = () => { selectedRegionID: selectedRegionId, }); - if (typesError || regionsError || versionLoadError) { + if (typesError || regionsError || versionsError) { // This information is necessary to create a Cluster. Otherwise, show an error state. return ; } @@ -389,14 +373,10 @@ export const CreateCluster = () => { disableClearable={!!version} errorText={errorMap.k8s_version} label="Kubernetes Version" + loading={isLoadingVersions} options={versions} placeholder={' '} value={versions.find((v) => v.value === version) ?? null} - loading={ - versionLoading || - (isLkeEnterpriseLAFeatureEnabled && - enterpriseTierVersionDataIsLoading) - } /> {showAPL && ( <> diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx index f84cac678e6..64d5865f4ed 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx @@ -82,6 +82,7 @@ export const KubernetesClusterDetail = () => { { - const { clusterID, clusterLabel, currentVersion } = props; - const { data: versions } = useKubernetesVersionQuery(); + const { clusterID, clusterLabel, clusterTier, currentVersion } = props; + + const { versions } = useLkeStandardOrEnterpriseVersions(clusterTier); const nextVersion = getNextVersion(currentVersion, versions ?? []); const [dialogOpen, setDialogOpen] = React.useState(false); @@ -51,6 +57,7 @@ export const UpgradeKubernetesVersionBanner = (props: Props) => { setDialogOpen(false)} diff --git a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx index 9048c927dac..7d93b7d3b6d 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx @@ -32,7 +32,7 @@ import { DeleteKubernetesClusterDialog } from '../KubernetesClusterDetail/Delete import UpgradeVersionModal from '../UpgradeVersionModal'; import { KubernetesEmptyState } from './KubernetesLandingEmptyState'; -import type { KubeNodePoolResponse } from '@linode/api-v4'; +import type { KubeNodePoolResponse, KubernetesTier } from '@linode/api-v4'; interface ClusterDialogState { loading: boolean; @@ -47,6 +47,7 @@ interface UpgradeDialogState { open: boolean; selectedClusterID: number; selectedClusterLabel: string; + selectedClusterTier: KubernetesTier; } const defaultDialogState = { @@ -63,6 +64,7 @@ const defaultUpgradeDialogState = { open: false, selectedClusterID: 0, selectedClusterLabel: '', + selectedClusterTier: 'standard' as KubernetesTier, }; const preferenceKey = 'kubernetes'; @@ -113,6 +115,7 @@ export const KubernetesLanding = () => { const openUpgradeDialog = ( clusterID: number, clusterLabel: string, + clusterTier: KubernetesTier, currentVersion: string ) => { setUpgradeDialogState({ @@ -120,6 +123,7 @@ export const KubernetesLanding = () => { open: true, selectedClusterID: clusterID, selectedClusterLabel: clusterLabel, + selectedClusterTier: clusterTier, }); }; @@ -244,6 +248,7 @@ export const KubernetesLanding = () => { openUpgradeDialog( cluster.id, cluster.label, + cluster?.tier ?? 'standard', // TODO LKE: remove fallback once LKE-E is in GA and tier is required cluster.k8s_version ) } @@ -272,6 +277,7 @@ export const KubernetesLanding = () => { void; } export const UpgradeDialog = (props: Props) => { - const { clusterID, clusterLabel, currentVersion, isOpen, onClose } = props; + const { + clusterID, + clusterLabel, + clusterTier, + currentVersion, + isOpen, + onClose, + } = props; - const { data: versions } = useKubernetesVersionQuery(); const { enqueueSnackbar } = useSnackbar(); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( clusterID ); + const { versions } = useLkeStandardOrEnterpriseVersions(clusterTier); + const nextVersion = getNextVersion(currentVersion, versions ?? []); const [hasUpdatedSuccessfully, setHasUpdatedSuccessfully] = React.useState( diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts index bc122c879f3..724edc5e946 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts @@ -6,19 +6,33 @@ import { linodeTypeFactory, nodePoolFactory, } from 'src/factories'; +import { + kubernetesEnterpriseTierVersionFactory, + kubernetesVersionFactory, +} from 'src/factories'; import { extendType } from 'src/utilities/extendType'; import { getLatestVersion, + getNextVersion, getTotalClusterMemoryCPUAndStorage, useAPLAvailability, useIsLkeEnterpriseEnabled, + useLkeStandardOrEnterpriseVersions, } from './kubeUtils'; +import { KubernetesTieredVersion, KubernetesVersion } from '@linode/api-v4'; + +const mockKubernetesVersions = kubernetesVersionFactory.buildList(1); +const mockKubernetesEnterpriseVersions = kubernetesEnterpriseTierVersionFactory.buildList( + 1 +); const queryMocks = vi.hoisted(() => ({ useAccount: vi.fn().mockReturnValue({}), useAccountBetaQuery: vi.fn().mockReturnValue({}), useFlags: vi.fn().mockReturnValue({}), + useKubernetesTieredVersionsQuery: vi.fn().mockReturnValue({}), + useKubernetesVersionQuery: vi.fn().mockReturnValue({}), })); vi.mock('src/queries/account/account', () => { @@ -45,6 +59,16 @@ vi.mock('src/hooks/useFlags', () => { }; }); +vi.mock('src/queries/kubernetes', () => { + const actual = vi.importActual('src/queries/kubernetes'); + return { + ...actual, + useKubernetesTieredVersionsQuery: + queryMocks.useKubernetesTieredVersionsQuery, + useKubernetesVersionQuery: queryMocks.useKubernetesVersionQuery, + }; +}); + afterEach(() => { vi.clearAllMocks(); }); @@ -137,6 +161,16 @@ describe('helper functions', () => { expect(result).toEqual({ label: '2.00', value: '2.00' }); }); + it('should return the correct latest version from a list of enterprise versions', () => { + const enterpriseVersions = [ + { label: '1.31.1+lke1', value: '1.31.1+lke1' }, + { label: '1.31.1+lke2', value: '1.31.1+lke2' }, + { label: '1.32.1+lke1', value: '1.32.1+lke1' }, + ]; + const result = getLatestVersion(enterpriseVersions); + expect(result).toEqual({ label: '1.32.1+lke1', value: '1.32.1+lke1' }); + }); + it('should handle latest version minor version correctly', () => { const versions = [ { label: '1.22', value: '1.22' }, @@ -146,6 +180,7 @@ describe('helper functions', () => { const result = getLatestVersion(versions); expect(result).toEqual({ label: '1.30', value: '1.30' }); }); + it('should handle latest patch version correctly', () => { const versions = [ { label: '1.22', value: '1.30' }, @@ -156,80 +191,186 @@ describe('helper functions', () => { const result = getLatestVersion(versions); expect(result).toEqual({ label: '1.50.1', value: '1.50.1' }); }); + it('should return default fallback value when called with empty versions', () => { const result = getLatestVersion([]); expect(result).toEqual({ label: '', value: '' }); }); }); + + describe('getNextVersion', () => { + it('should get the next version when given a current standard version', () => { + const versions: KubernetesVersion[] = [ + { id: '1.00' }, + { id: '1.10' }, + { id: '2.00' }, + ]; + const currentVersion = '1.10'; + + const result = getNextVersion(currentVersion, versions); + expect(result).toEqual('2.00'); + }); + }); + + it('should get the next version when given a current enterprise version', () => { + const versions: KubernetesTieredVersion[] = [ + { id: '1.31.1+lke1', tier: 'enterprise' }, + { id: '1.31.1+lke2', tier: 'enterprise' }, + { id: '1.32.1+lke1', tier: 'enterprise' }, + ]; + const currentVersion = '1.31.1+lke2'; + + const result = getNextVersion(currentVersion, versions); + expect(result).toEqual('1.32.1+lke1'); + }); + + it('should get the next version when given an obsolete current version', () => { + const versions: KubernetesVersion[] = [ + { id: '1.16' }, + { id: '1.17' }, + { id: '1.18' }, + ]; + const currentVersion = '1.15'; + + const result = getNextVersion(currentVersion, versions); + expect(result).toEqual('1.16'); + }); }); -describe('useIsLkeEnterpriseEnabled', () => { - it('returns false for feature enablement if the account does not have the capability', () => { - queryMocks.useAccount.mockReturnValue({ - data: { - capabilities: [], - }, +describe('hooks', () => { + describe('useIsLkeEnterpriseEnabled', () => { + it('returns false for feature enablement if the account does not have the capability', () => { + queryMocks.useAccount.mockReturnValue({ + data: { + capabilities: [], + }, + }); + queryMocks.useFlags.mockReturnValue({ + lkeEnterprise: { + enabled: true, + ga: true, + la: true, + }, + }); + + const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); + expect(result.current).toStrictEqual({ + isLkeEnterpriseGAFeatureEnabled: false, + isLkeEnterpriseGAFlagEnabled: true, + isLkeEnterpriseLAFeatureEnabled: false, + isLkeEnterpriseLAFlagEnabled: true, + }); }); - queryMocks.useFlags.mockReturnValue({ - lkeEnterprise: { - enabled: true, - ga: true, - la: true, - }, + + it('returns true for LA feature enablement if the account has the capability + enabled LA feature flag values', () => { + queryMocks.useAccount.mockReturnValue({ + data: { + capabilities: ['Kubernetes Enterprise'], + }, + }); + queryMocks.useFlags.mockReturnValue({ + lkeEnterprise: { + enabled: true, + ga: false, + la: true, + }, + }); + + const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); + expect(result.current).toStrictEqual({ + isLkeEnterpriseGAFeatureEnabled: false, + isLkeEnterpriseGAFlagEnabled: false, + isLkeEnterpriseLAFeatureEnabled: true, + isLkeEnterpriseLAFlagEnabled: true, + }); }); - const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); - expect(result.current).toStrictEqual({ - isLkeEnterpriseGAFeatureEnabled: false, - isLkeEnterpriseGAFlagEnabled: true, - isLkeEnterpriseLAFeatureEnabled: false, - isLkeEnterpriseLAFlagEnabled: true, + it('returns true for GA feature enablement if the account has the capability + enabled GA feature flag values', () => { + queryMocks.useAccount.mockReturnValue({ + data: { + capabilities: ['Kubernetes Enterprise'], + }, + }); + queryMocks.useFlags.mockReturnValue({ + lkeEnterprise: { + enabled: true, + ga: true, + la: true, + }, + }); + + const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); + expect(result.current).toStrictEqual({ + isLkeEnterpriseGAFeatureEnabled: true, + isLkeEnterpriseGAFlagEnabled: true, + isLkeEnterpriseLAFeatureEnabled: true, + isLkeEnterpriseLAFlagEnabled: true, + }); }); }); - it('returns true for LA feature enablement if the account has the capability + enabled LA feature flag values', () => { - queryMocks.useAccount.mockReturnValue({ - data: { - capabilities: ['Kubernetes Enterprise'], - }, + describe('useLkeStandardOrEnterpriseVersions', () => { + beforeAll(() => { + queryMocks.useAccount.mockReturnValue({ + data: { + capabilities: ['Kubernetes Enterprise'], + }, + }); + queryMocks.useFlags.mockReturnValue({ + lkeEnterprise: { + enabled: true, + ga: true, + la: true, + }, + }); + queryMocks.useKubernetesTieredVersionsQuery.mockReturnValue({ + data: mockKubernetesEnterpriseVersions, + error: null, + isFetching: false, + }); + queryMocks.useKubernetesVersionQuery.mockReturnValue({ + data: mockKubernetesVersions, + error: null, + isLoading: false, + }); }); - queryMocks.useFlags.mockReturnValue({ - lkeEnterprise: { - enabled: true, - ga: false, - la: true, - }, + + it('returns enterprise versions for enterprise clusters when the LKE-E feature is enabled', () => { + const { result } = renderHook(() => + useLkeStandardOrEnterpriseVersions('enterprise') + ); + + expect(result.current.versions).toEqual(mockKubernetesEnterpriseVersions); + expect(result.current.isLoadingVersions).toBe(false); + expect(result.current.versionsError).toBe(null); }); - const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); - expect(result.current).toStrictEqual({ - isLkeEnterpriseGAFeatureEnabled: false, - isLkeEnterpriseGAFlagEnabled: false, - isLkeEnterpriseLAFeatureEnabled: true, - isLkeEnterpriseLAFlagEnabled: true, + it('returns standard versions for standard clusters when the LKE-E feature is enabled', () => { + const { result } = renderHook(() => + useLkeStandardOrEnterpriseVersions('standard') + ); + + expect(result.current.versions).toEqual(mockKubernetesVersions); + expect(result.current.isLoadingVersions).toBe(false); + expect(result.current.versionsError).toBe(null); }); - }); - it('returns true for GA feature enablement if the account has the capability + enabled GA feature flag values', () => { - queryMocks.useAccount.mockReturnValue({ - data: { - capabilities: ['Kubernetes Enterprise'], - }, - }); - queryMocks.useFlags.mockReturnValue({ - lkeEnterprise: { - enabled: true, - ga: true, - la: true, - }, - }); - - const { result } = renderHook(() => useIsLkeEnterpriseEnabled()); - expect(result.current).toStrictEqual({ - isLkeEnterpriseGAFeatureEnabled: true, - isLkeEnterpriseGAFlagEnabled: true, - isLkeEnterpriseLAFeatureEnabled: true, - isLkeEnterpriseLAFlagEnabled: true, + it('returns standard versions when the LKE-E feature is disabled', () => { + queryMocks.useFlags.mockReturnValue({ + lkeEnterprise: { + enabled: false, + ga: true, + la: true, + }, + }); + + const { result } = renderHook(() => + useLkeStandardOrEnterpriseVersions('standard') + ); + + expect(result.current.versions).toEqual(mockKubernetesVersions); + expect(result.current.isLoadingVersions).toBe(false); + expect(result.current.versionsError).toBe(null); }); }); }); diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.ts b/packages/manager/src/features/Kubernetes/kubeUtils.ts index 7d8e6c84aec..b3f8baaa517 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.ts @@ -1,6 +1,10 @@ import { useFlags } from 'src/hooks/useFlags'; import { useAccount } from 'src/queries/account/account'; import { useAccountBetaQuery } from 'src/queries/account/betas'; +import { + useKubernetesTieredVersionsQuery, + useKubernetesVersionQuery, +} from 'src/queries/kubernetes'; import { isFeatureEnabledV2 } from 'src/utilities/accountCapabilities'; import { getBetaStatus } from 'src/utilities/betaUtils'; import { sortByVersion } from 'src/utilities/sort-by'; @@ -9,6 +13,8 @@ import type { Account } from '@linode/api-v4/lib/account'; import type { KubeNodePoolResponse, KubernetesCluster, + KubernetesTier, + KubernetesTieredVersion, KubernetesVersion, } from '@linode/api-v4/lib/kubernetes'; import type { Region } from '@linode/api-v4/lib/regions'; @@ -66,14 +72,19 @@ export const getDescriptionForCluster = ( return description.join(', '); }; +/** + * Finds the next version for upgrade, given a current version and the list of all versions. + * @param currentVersion The current cluster version + * @param versions All available standard or enterprise versions + * @returns The next version from which to upgrade from the current version + */ export const getNextVersion = ( currentVersion: string, - versions: KubernetesVersion[] + versions: KubernetesTieredVersion[] | KubernetesVersion[] // TODO LKE-E: remove KubernetesVersion from type after GA. ) => { if (versions.length === 0) { return null; } - const versionStrings = versions.map((v) => v.id).sort(); const currentIdx = versionStrings.findIndex( (thisVersion) => currentVersion === thisVersion @@ -222,3 +233,45 @@ export const useIsLkeEnterpriseEnabled = () => { isLkeEnterpriseLAFlagEnabled, }; }; + +/** + * @todo Remove this hook and just use `useKubernetesTieredVersionsQuery` directly once we're in GA + * since we'll always have a cluster tier. + * + * A hook to return the correct list of versions depending on the LKE cluster tier. + * @param clusterTier Whether the cluster is standard or enterprise + * @returns The list of either standard or enterprise k8 versions and query loading or error state + */ +export const useLkeStandardOrEnterpriseVersions = ( + clusterTier: KubernetesTier +) => { + const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); + + /** + * If LKE-E is enabled, use the data from the new /versions/ endpoint for enterprise tiers. + * If LKE-E is disabled, use the data from the existing /versions endpoint and disable the tiered query. + */ + const { + data: enterpriseTierVersions, + error: enterpriseTierVersionsError, + isFetching: enterpriseTierVersionsIsLoading, + } = useKubernetesTieredVersionsQuery( + 'enterprise', + isLkeEnterpriseLAFeatureEnabled + ); + + const { + data: _versions, + error: versionsError, + isLoading: versionsIsLoading, + } = useKubernetesVersionQuery(); + + return { + isLoadingVersions: enterpriseTierVersionsIsLoading || versionsIsLoading, + versions: + isLkeEnterpriseLAFeatureEnabled && clusterTier === 'enterprise' + ? enterpriseTierVersions + : _versions, + versionsError: enterpriseTierVersionsError || versionsError, + }; +}; diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 7490451b59a..a17492ec73d 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -381,10 +381,14 @@ export const useKubernetesVersionQuery = () => ...queryPresets.oneTimeFetch, }); -export const useKubernetesTieredVersionsQuery = (tier: string) => { +export const useKubernetesTieredVersionsQuery = ( + tier: string, + enabled = true +) => { return useQuery({ ...kubernetesQueries.tieredVersions(tier), ...queryPresets.oneTimeFetch, + enabled, }); };