Skip to content

Commit

Permalink
upcoming: [M3-8960] - Update Kubernetes version upgrade components fo…
Browse files Browse the repository at this point in the history
…r LKE-E (linode#11415)

* Make a hook because this is getting repetitive

* Determine enterprise or standard versions with hook

* Update versions type for util - need to test

* Improve hook and use in cluster create flow

* Move hook to kubeUtils

* Add test coverage and factories

* Clean up kubeUtils.test.ts

* Add kubeUtils helper function test coverage for LKE-E

* Update Cypress test coverage for enterprise version upgrades

* Clean up

* Added changeset: Update Kubernetes version upgrade components for LKE-E

* Fix test failure by mocking the LKE-E capability needed

* Update TODO

* Address PR feedback: increment ids in tiered version factories
  • Loading branch information
mjac0bs authored and dmcintyr-akamai committed Jan 9, 2025
1 parent c1241a2 commit eb48313
Show file tree
Hide file tree
Showing 12 changed files with 532 additions and 106 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Update Kubernetes version upgrade components for LKE-E ([#11415](https://github.com/linode/manager/pull/11415))
207 changes: 205 additions & 2 deletions packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
mockGetControlPlaneACL,
mockUpdateControlPlaneACLError,
mockGetControlPlaneACLError,
mockGetTieredKubernetesVersions,
} from 'support/intercepts/lke';
import {
mockGetLinodeType,
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions packages/manager/src/factories/kubernetesCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
KubernetesControlPlaneACLPayload,
KubernetesDashboardResponse,
KubernetesEndpointResponse,
KubernetesTieredVersion,
KubernetesVersion,
PoolNodeResponse,
} from '@linode/api-v4/lib/kubernetes/types';
Expand Down Expand Up @@ -78,6 +79,20 @@ export const kubernetesVersionFactory = Factory.Sync.makeFactory<KubernetesVersi
}
);

export const kubernetesStandardTierVersionFactory = Factory.Sync.makeFactory<KubernetesTieredVersion>(
{
id: Factory.each((id) => `'v1.3${id}'`),
tier: 'standard',
}
);

export const kubernetesEnterpriseTierVersionFactory = Factory.Sync.makeFactory<KubernetesTieredVersion>(
{
id: Factory.each((id) => `'v1.31.${id}+lke1'`),
tier: 'enterprise',
}
);

export const kubernetesControlPlaneACLOptionsFactory = Factory.Sync.makeFactory<ControlPlaneACLOptions>(
{
addresses: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,20 +8,20 @@ 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';

import {
getNextVersion,
getTotalClusterMemoryCPUAndStorage,
useLkeStandardOrEnterpriseVersions,
} from '../kubeUtils';
import { ClusterActionMenu } from './ClusterActionMenu';

import type { KubeNodePoolResponse, KubernetesCluster } from '@linode/api-v4';

const useStyles = makeStyles()(() => ({
clusterRow: {
'&:before': {
Expand Down Expand Up @@ -64,14 +63,17 @@ 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);
const { data: regions } = useRegionsQuery();

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getLatestVersion,
useAPLAvailability,
useIsLkeEnterpriseEnabled,
useLkeStandardOrEnterpriseVersions,
} from 'src/features/Kubernetes/kubeUtils';
import { useAccount } from 'src/queries/account/account';
import {
Expand All @@ -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';
Expand Down Expand Up @@ -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/<tier> 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,
Expand Down Expand Up @@ -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 <ErrorState errorText="An unexpected error occurred." />;
}
Expand Down Expand Up @@ -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 && (
<>
Expand Down
Loading

0 comments on commit eb48313

Please sign in to comment.