diff --git a/packages/api-v4/.changeset/pr-11347-fixed-1733327446988.md b/packages/api-v4/.changeset/pr-11347-fixed-1733327446988.md new file mode 100644 index 00000000000..d21cf788e93 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11347-fixed-1733327446988.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Fixed +--- + +Nullable AccountBeta ended & description properties ([#11347](https://github.com/linode/manager/pull/11347)) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index d23ded3eb0d..08b575c7606 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -607,8 +607,8 @@ export interface AccountBeta { label: string; started: string; id: string; - ended?: string; - description?: string; + ended: string | null; + description: string | null; /** * The datetime the account enrolled into the beta * @example 2024-10-23T14:22:29 diff --git a/packages/manager/.changeset/pr-11347-tests-1733170030162.md b/packages/manager/.changeset/pr-11347-tests-1733170030162.md new file mode 100644 index 00000000000..261f1ae08b3 --- /dev/null +++ b/packages/manager/.changeset/pr-11347-tests-1733170030162.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Mock LKE creation flow + APL coverage ([#11347](https://github.com/linode/manager/pull/11347)) diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index 4780133b78f..c23a44f3d55 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -353,7 +353,6 @@ describe('Billing Activity Feed', () => { // reflect each timezone. timeZonesList.forEach((timezone) => { const timezoneId = timezone.key; - const humanReadable = timezone.human; mockUpdateProfile({ ...mockProfile, @@ -367,7 +366,7 @@ describe('Billing Activity Feed', () => { cy.findByText('Timezone') .should('be.visible') .click() - .type(`${humanReadable}{enter}`); + .type('Pacific Time - Los Angeles{enter}'); ui.button .findByTitle('Update Timezone') diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 6c93301d30b..cd9c4c9b00d 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -3,32 +3,35 @@ */ import { accountFactory, + dedicatedTypeFactory, kubernetesClusterFactory, kubernetesControlPlaneACLFactory, kubernetesControlPlaneACLOptionsFactory, linodeTypeFactory, regionFactory, + nodePoolFactory, + kubeLinodeFactory, } from 'src/factories'; import { mockCreateCluster, mockGetCluster, mockCreateClusterError, mockGetControlPlaneACL, + mockGetClusterPools, + mockGetDashboardUrl, + mockGetApiEndpoints, + mockGetClusters, + mockGetLKEClusterTypes, } from 'support/intercepts/lke'; +import { mockGetAccountBeta } from 'support/intercepts/betas'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetRegions, mockGetRegionAvailability, } from 'support/intercepts/regions'; -import { KubernetesCluster } from '@linode/api-v4'; -import { LkePlanDescription } from 'support/api/lke'; -import { lkeClusterPlans } from 'support/constants/lke'; -import { chooseRegion, getRegionById } from 'support/util/regions'; -import { interceptCreateCluster } from 'support/intercepts/lke'; +import { getRegionById } from 'support/util/regions'; import { ui } from 'support/ui'; import { randomLabel, randomNumber, randomItem } from 'support/util/random'; -import { cleanUp } from 'support/util/cleanup'; -import { authenticate } from 'support/api/authentication'; import { dcPricingLkeCheckoutSummaryPlaceholder, dcPricingLkeHAPlaceholder, @@ -40,77 +43,141 @@ import { } from 'support/constants/dc-specific-pricing'; import { mockGetLinodeTypes } from 'support/intercepts/linodes'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { chooseRegion } from 'support/util/regions'; +import { getTotalClusterMemoryCPUAndStorage } from 'src/features/Kubernetes/kubeUtils'; +import { getTotalClusterPrice } from 'src/utilities/pricing/kubernetes'; -/** - * Gets the label for an LKE plan as shown in creation plan table. - * - * @param clusterPlan - Cluster plan from which to determine Cloud Manager LKE plan name. - * - * @returns LKE plan name for plan. - */ -const getLkePlanName = (clusterPlan: LkePlanDescription) => { - return `${clusterPlan.type} ${clusterPlan.size} GB`; -}; +import type { ExtendedType } from 'src/utilities/extendType'; +import type { LkePlanDescription } from 'support/api/lke'; +import { PriceType } from '@linode/api-v4/lib/types'; -/** - * Gets the label for an LKE plan as shown in the creation checkout bar. - * - * @param clusterPlan - Cluster plan from which to determine Cloud Manager LKE checkout name. - * - * @returns LKE checkout plan name for plan. - */ -const getLkePlanCheckoutName = (clusterPlan: LkePlanDescription) => { - return `${clusterPlan.type} ${clusterPlan.size} GB Plan`; -}; +const dedicatedNodeCount = 4; +const nanodeNodeCount = 3; -/** - * Returns each plan in an array which is similar to the given plan. - * - * Plans are considered similar if they have identical type and size. - * - * @param clusterPlan - Cluster plan with which to compare similarity. - * @param clusterPlans - Array from which to find similar cluster plans. - * - * @returns Array of similar cluster plans. - */ -const getSimilarPlans = ( - clusterPlan: LkePlanDescription, - clusterPlans: LkePlanDescription[] -) => { - return clusterPlans.filter((otherClusterPlan: any) => { - return ( - clusterPlan.type === otherClusterPlan.type && - clusterPlan.size === otherClusterPlan.size - ); - }); -}; +const clusterRegion = chooseRegion({ + capabilities: ['Kubernetes'], +}); +const dedicatedCpuPool = nodePoolFactory.build({ + count: dedicatedNodeCount, + nodes: kubeLinodeFactory.buildList(dedicatedNodeCount), + type: 'g6-dedicated-2', +}); +const nanodeMemoryPool = nodePoolFactory.build({ + count: nanodeNodeCount, + nodes: kubeLinodeFactory.buildList(nanodeNodeCount), + type: 'g6-nanode-1', +}); +const dedicatedType = dedicatedTypeFactory.build({ + disk: 81920, + id: 'g6-dedicated-2', + label: 'Dedicated 4 GB', + memory: 4096, + price: { + hourly: 0.054, + monthly: 36.0, + }, + region_prices: dcPricingMockLinodeTypes.find( + (type) => type.id === 'g6-dedicated-2' + )?.region_prices, + vcpus: 2, +}) as ExtendedType; +const nanodeType = linodeTypeFactory.build({ + disk: 25600, + id: 'g6-nanode-1', + label: 'Linode 2 GB', + memory: 2048, + price: { + hourly: 0.0075, + monthly: 5.0, + }, + region_prices: dcPricingMockLinodeTypes.find( + (type) => type.id === 'g6-nanode-1' + )?.region_prices, + vcpus: 1, +}) as ExtendedType; +const mockedLKEClusterPrices: PriceType[] = [ + { + id: 'lke-sa', + label: 'LKE Standard Availability', + price: { + hourly: 0.0, + monthly: 0.0, + }, + region_prices: [], + transfer: 0, + }, +]; +const mockedLKEHAClusterPrices: PriceType[] = [ + { + id: 'lke-ha', + label: 'LKE High Availability', + price: { + hourly: 0.09, + monthly: 60.0, + }, + region_prices: [], + transfer: 0, + }, +]; -authenticate(); describe('LKE Cluster Creation', () => { - before(() => { - cleanUp(['linodes', 'lke-clusters']); - }); - /* * - Confirms that users can create a cluster by completing the LKE create form. * - Confirms that LKE cluster is created. * - Confirms that user is redirected to new LKE cluster summary page. + * - Confirms that correct information is shown on the LKE cluster summary page * - Confirms that new LKE cluster summary page shows expected node pools. * - Confirms that new LKE cluster is shown on LKE clusters landing page. - * - Confirms that correct information is shown on the LKE cluster summary page */ - it('can create an LKE cluster', () => { - cy.tag('method:e2e', 'purpose:dcTesting'); - const clusterLabel = randomLabel(); - const clusterRegion = chooseRegion({ - capabilities: ['Kubernetes'], - }); - const clusterVersion = '1.27'; - const clusterPlans = new Array(2) - .fill(null) - .map(() => randomItem(lkeClusterPlans)); + const clusterLabel = randomLabel(); + const clusterVersion = '1.27'; + const clusterPlans: LkePlanDescription[] = [ + { + nodeCount: dedicatedNodeCount, + planName: 'Dedicated 4 GB', + size: 4, + tab: 'Dedicated CPU', + type: 'dedicated', + }, + { + nodeCount: nanodeNodeCount, + planName: 'Linode 2 GB', + size: 24, + tab: 'Shared CPU', + type: 'nanode', + }, + ]; + const mockedLKECluster = kubernetesClusterFactory.build({ + label: clusterLabel, + region: clusterRegion.id, + }); + const mockedLKEClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; + const mockedLKEClusterControlPlane = kubernetesControlPlaneACLFactory.build(); + const mockedLKEClusterTypes = [dedicatedType, nanodeType]; + const { + CPU: totalCpu, + RAM: totalMemory, + Storage: totalStorage, + } = getTotalClusterMemoryCPUAndStorage( + mockedLKEClusterPools, + mockedLKEClusterTypes + ); - interceptCreateCluster().as('createCluster'); + it('can create an LKE cluster', () => { + mockCreateCluster(mockedLKECluster).as('createCluster'); + mockGetCluster(mockedLKECluster).as('getCluster'); + mockGetClusterPools(mockedLKECluster.id, mockedLKEClusterPools).as( + 'getClusterPools' + ); + mockGetDashboardUrl(mockedLKECluster.id).as('getDashboardUrl'); + mockGetControlPlaneACL( + mockedLKECluster.id, + mockedLKEClusterControlPlane + ).as('getControlPlaneACL'); + mockGetApiEndpoints(mockedLKECluster.id).as('getApiEndpoints'); + mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); + mockGetLKEClusterTypes(mockedLKEClusterPrices).as('getLKEClusterTypes'); + mockGetClusters([mockedLKECluster]).as('getClusters'); cy.visitWithLogin('/kubernetes/clusters'); @@ -123,44 +190,40 @@ describe('LKE Cluster Creation', () => { cy.url().should('endWith', '/kubernetes/create'); // Fill out LKE creation form label, region, and Kubernetes version fields. - cy.findByLabelText('Cluster Label') + cy.get('[data-qa-textfield-label="Cluster Label"]') .should('be.visible') - .click() - .type(`${clusterLabel}{enter}`); + .click(); + cy.focused().type(`${clusterLabel}{enter}`); ui.regionSelect.find().click().type(`${clusterRegion.label}{enter}`); - cy.findByText('Kubernetes Version') - .should('be.visible') + ui.autocomplete + .findByLabel('Kubernetes Version') .click() .type(`${clusterVersion}{enter}`); - cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); + cy.get('[data-testid="ha-radio-button-no"]').should('be.visible').click(); - let totalCpu = 0; - let totalMemory = 0; - let totalStorage = 0; let monthPrice = 0; - // Add a node pool for each randomly selected plan, and confirm that the + // Add a node pool for each selected plan, and confirm that the // selected node pool plan is added to the checkout bar. clusterPlans.forEach((clusterPlan) => { - const nodeCount = randomNumber(1, 3); - const planName = getLkePlanName(clusterPlan); - const checkoutName = getLkePlanCheckoutName(clusterPlan); + const nodeCount = clusterPlan.nodeCount; + const planName = clusterPlan.planName; - cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); + cy.log(`Adding ${nodeCount}x ${planName} node(s)`); // Click the right tab for the plan, and add a node pool with the desired // number of nodes. cy.findByText(clusterPlan.tab).should('be.visible').click(); + const quantityInput = '[name="Quantity"]'; cy.findByText(planName) .should('be.visible') .closest('tr') .within(() => { - cy.get('[name="Quantity"]') - .should('be.visible') - .click() - .type(`{selectall}${nodeCount}`); + cy.get(quantityInput).should('be.visible'); + cy.get(quantityInput).click(); + cy.get(quantityInput).type(`{selectall}${nodeCount}`); ui.button .findByTitle('Add') @@ -176,31 +239,16 @@ describe('LKE Cluster Creation', () => { // It's possible that multiple pools of the same type get added. // We're taking a naive approach here by confirming that at least one // instance of the pool appears in the checkout bar. - cy.findAllByText(checkoutName).first().should('be.visible'); + cy.findAllByText(`${planName} Plan`).first().should('be.visible'); }); - // Expected information on the LKE cluster summary page. - if (clusterPlan.size == 2 && clusterPlan.type == 'Linode') { - totalCpu = totalCpu + nodeCount * 1; - totalMemory = totalMemory + nodeCount * 2; - totalStorage = totalStorage + nodeCount * 50; - monthPrice = monthPrice + nodeCount * 12; - } - if (clusterPlan.size == 4 && clusterPlan.type == 'Linode') { - totalCpu = totalCpu + nodeCount * 2; - totalMemory = totalMemory + nodeCount * 4; - totalStorage = totalStorage + nodeCount * 80; - monthPrice = monthPrice + nodeCount * 24; - } - if (clusterPlan.size == 4 && clusterPlan.type == 'Dedicated') { - totalCpu = totalCpu + nodeCount * 2; - totalMemory = totalMemory + nodeCount * 4; - totalStorage = totalStorage + nodeCount * 80; - monthPrice = monthPrice + nodeCount * 36; - } + monthPrice = getTotalClusterPrice({ + highAvailabilityPrice: 0, + pools: [nanodeMemoryPool, dedicatedCpuPool], + region: clusterRegion.id, + types: mockedLKEClusterTypes, + }); }); - // $60.00/month for enabling HA control plane - const totalPrice = monthPrice + 60; // Create LKE cluster. cy.get('[data-testid="kube-checkout-bar"]') @@ -215,31 +263,40 @@ describe('LKE Cluster Creation', () => { // Wait for LKE cluster to be created and confirm that we are redirected // to the cluster summary page. - cy.wait('@createCluster').then(({ response }) => { - if (!response) { - throw new Error( - `Error creating LKE cluster ${clusterLabel}; API request failed` - ); - } - const cluster: KubernetesCluster = response.body; - cy.url().should('endWith', `/kubernetes/clusters/${cluster.id}/summary`); - }); + cy.wait([ + '@getCluster', + '@getClusterPools', + '@createCluster', + '@getLKEClusterTypes', + '@getLinodeTypes', + '@getDashboardUrl', + '@getControlPlaneACL', + '@getApiEndpoints', + ]); + cy.url().should( + 'endWith', + `/kubernetes/clusters/${mockedLKECluster.id}/summary` + ); // Confirm that each node pool is shown. clusterPlans.forEach((clusterPlan) => { // Because multiple node pools may have identical labels, we figure out // how many identical labels for each plan will exist and confirm that // the expected number is present. - const nodePoolLabel = getLkePlanName(clusterPlan); + const nodePoolLabel = clusterPlan.planName; const similarNodePoolCount = getSimilarPlans(clusterPlan, clusterPlans) .length; - //Confirm that the cluster created with the expected parameters. + // Confirm that the cluster created with the expected parameters. cy.findAllByText(`${clusterRegion.label}`).should('be.visible'); cy.findAllByText(`${totalCpu} CPU Cores`).should('be.visible'); - cy.findAllByText(`${totalMemory} GB RAM`).should('be.visible'); - cy.findAllByText(`${totalStorage} GB Storage`).should('be.visible'); - cy.findAllByText(`$${totalPrice}.00/month`).should('be.visible'); + cy.findAllByText(`${Math.round(totalStorage / 1024)} GB Storage`).should( + 'be.visible' + ); + cy.findAllByText(`${Math.round(totalMemory / 1024)} GB RAM`).should( + 'be.visible' + ); + cy.findAllByText(`$${monthPrice.toFixed(2)}/month`).should('be.visible'); cy.contains('Kubernetes API Endpoint').should('be.visible'); cy.contains('linodelke.net:443').should('be.visible'); @@ -249,26 +306,169 @@ describe('LKE Cluster Creation', () => { .should('be.visible'); }); - // Navigate to the LKE landing page and confirm that new cluster is shown. ui.breadcrumb .find() .should('be.visible') .within(() => { cy.findByText(clusterLabel).should('be.visible'); + }); + }); +}); + +describe('LKE Cluster Creation with APL enabled', () => { + it('can create an LKE cluster with APL flag enabled', () => { + const clusterLabel = randomLabel(); + const mockedLKECluster = kubernetesClusterFactory.build({ + label: clusterLabel, + region: clusterRegion.id, + }); + const mockedLKEClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; + const mockedLKEClusterControlPlane = kubernetesControlPlaneACLFactory.build(); + const dedicated4Type = dedicatedTypeFactory.build({ + disk: 163840, + id: 'g6-dedicated-4', + label: 'Dedicated 8GB', + memory: 8192, + price: { + hourly: 0.108, + monthly: 72.0, + }, + region_prices: dcPricingMockLinodeTypes.find( + (type) => type.id === 'g6-dedicated-8' + )?.region_prices, + vcpus: 4, + }); + const dedicated8Type = dedicatedTypeFactory.build({ + disk: 327680, + id: 'g6-dedicated-8', + label: 'Dedicated 16GB', + memory: 16384, + price: { + hourly: 0.216, + monthly: 144.0, + }, + region_prices: dcPricingMockLinodeTypes.find( + (type) => type.id === 'g6-dedicated-8' + )?.region_prices, + vcpus: 8, + }); + const mockedLKEClusterTypes = [ + dedicatedType, + dedicated4Type, + dedicated8Type, + nanodeType, + ]; + mockAppendFeatureFlags({ + apl: { + enabled: true, + }, + }).as('getFeatureFlags'); + mockGetAccountBeta({ + id: 'apl', + label: 'Akamai App Platform Beta', + enrolled: '2024-11-04T21:39:41', + description: + 'Akamai App Platform is a platform that combines developer and operations-centric tools, automation and self-service to streamline the application lifecycle when using Kubernetes. This process will pre-register you for an upcoming beta.', + started: '2024-10-31T18:00:00', + ended: null, + }).as('getAccountBeta'); + mockCreateCluster(mockedLKECluster).as('createCluster'); + mockGetCluster(mockedLKECluster).as('getCluster'); + mockGetClusterPools(mockedLKECluster.id, mockedLKEClusterPools).as( + 'getClusterPools' + ); + mockGetDashboardUrl(mockedLKECluster.id).as('getDashboardUrl'); + mockGetControlPlaneACL( + mockedLKECluster.id, + mockedLKEClusterControlPlane + ).as('getControlPlaneACL'); + mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); + mockGetLKEClusterTypes(mockedLKEHAClusterPrices).as('getLKEClusterTypes'); + mockGetApiEndpoints(mockedLKECluster.id).as('getApiEndpoints'); + + cy.visitWithLogin('/kubernetes/create'); + + cy.wait([ + '@getFeatureFlags', + '@getAccountBeta', + '@getLinodeTypes', + '@getLKEClusterTypes', + ]); + + // Enter cluster details + cy.get('[data-qa-textfield-label="Cluster Label"]') + .should('be.visible') + .click(); + cy.focused().type(`${clusterLabel}{enter}`); + + ui.regionSelect.find().click().type(`${clusterRegion.label}{enter}`); + + cy.findByTestId('apl-label').should('have.text', 'Akamai App Platform'); + cy.findByTestId('apl-radio-button-yes').should('be.visible').click(); + cy.findByTestId('ha-radio-button-yes').should('be.disabled'); + cy.get( + '[aria-label="Enabled by default when Akamai App Platform is enabled."]' + ).should('be.visible'); + + // Check that Shared CPU plans are disabled + ui.tabList.findTabByTitle('Shared CPU').click(); + cy.findByText( + 'Shared CPU instances are currently not available for Akamai App Platform.' + ).should('be.visible'); + cy.get('[data-qa-plan-row="Linode 2 GB"]').should('have.attr', 'disabled'); + + // Check that Dedicated CPU plans are available if greater than 8GB + ui.tabList.findTabByTitle('Dedicated CPU').click(); + cy.get('[data-qa-plan-row="Dedicated 4 GB"]').should( + 'have.attr', + 'disabled' + ); + cy.get('[data-qa-plan-row="Dedicated 8 GB"]').should( + 'not.have.attr', + 'disabled' + ); + cy.get('[data-qa-plan-row="Dedicated 16 GB"]').within(() => { + cy.get('[name="Quantity"]').click(); + cy.get('[name="Quantity"]').type('{selectall}3'); + + ui.button + .findByTitle('Add') + .should('be.visible') + .should('be.enabled') + .click(); + }); - cy.findByText('kubernetes').should('be.visible').click(); + // Check that the checkout bar displays the correct information + cy.get('[data-testid="kube-checkout-bar"]') + .should('be.visible') + .within(() => { + cy.findByText(`Dedicated 16 GB Plan`).should('be.visible'); + cy.findByText('$432.00').should('be.visible'); + cy.findByText('High Availability (HA) Control Plane').should( + 'be.visible' + ); + cy.findByText('$60.00/month').should('be.visible'); + cy.findByText('$492.00').should('be.visible'); + + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled') + .click(); }); - cy.url().should('endWith', '/kubernetes/clusters'); - cy.findByText(clusterLabel).should('be.visible'); + cy.wait([ + '@createCluster', + '@getCluster', + '@getClusterPools', + '@getDashboardUrl', + '@getControlPlaneACL', + '@getApiEndpoints', + ]); }); }); describe('LKE Cluster Creation with DC-specific pricing', () => { - before(() => { - cleanUp('lke-clusters'); - }); - /* * - Confirms that DC-specific prices are present in the LKE create form. * - Confirms that pricing docs link is shown in "Region" section. @@ -279,7 +479,6 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { it('can dynamically update prices when creating an LKE cluster based on region', () => { const dcSpecificPricingRegion = getRegionById('us-east'); const clusterLabel = randomLabel(); - const clusterVersion = '1.27'; const clusterPlans = new Array(2) .fill(null) .map(() => randomItem(dcPricingLkeClusterPlans)); @@ -322,18 +521,16 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { .click() .type(`${clusterLabel}{enter}`); - ui.regionSelect.find().type(`${dcSpecificPricingRegion.label}{enter}`); + ui.regionSelect + .find() + .click() + .type(`${dcSpecificPricingRegion.label}{enter}`); // Confirm that HA price updates dynamically once region selection is made. cy.contains(/\$.*\/month/).should('be.visible'); cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); - cy.findByText('Kubernetes Version') - .should('be.visible') - .click() - .type(`${clusterVersion}{enter}`); - // Confirm that with region and HA selections, create button is still disabled until plan selection is made. cy.get('[data-qa-deploy-linode]') .should('contain.text', 'Create Cluster') @@ -343,10 +540,9 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { // selected node pool plan is added to the checkout bar. clusterPlans.forEach((clusterPlan) => { const nodeCount = randomNumber(1, 3); - const planName = getLkePlanName(clusterPlan); - const checkoutName = getLkePlanCheckoutName(clusterPlan); + const planName = clusterPlan.planName; - cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); + cy.log(`Adding ${nodeCount}x ${clusterPlan.planName} node(s)`); // Click the right tab for the plan, and add a node pool with the desired // number of nodes. cy.findByText(clusterPlan.tab).should('be.visible').click(); @@ -373,7 +569,7 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { // It's possible that multiple pools of the same type get added. // We're taking a naive approach here by confirming that at least one // instance of the pool appears in the checkout bar. - cy.findAllByText(checkoutName).first().should('be.visible'); + cy.findAllByText(`${planName} Plan`).first().should('be.visible'); }); }); @@ -512,7 +708,6 @@ describe('LKE Cluster Creation with ACL', () => { .should('be.visible'); // Add a node pool - cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -647,7 +842,6 @@ describe('LKE Cluster Creation with ACL', () => { .click(); // Add a node pool - cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -785,7 +979,6 @@ describe('LKE Cluster Creation with ACL', () => { cy.contains('Must be a valid IPv6 address.').should('not.exist'); // Add a node pool - cy.log(`Adding ${nodeCount}x ${getLkePlanName(clusterPlan)} node(s)`); cy.findByText(clusterPlan.tab).should('be.visible').click(); cy.findByText(planName) .should('be.visible') @@ -964,3 +1157,25 @@ describe('LKE Cluster Creation with LKE-E', () => { }); }); }); + +/** + * Returns each plan in an array which is similar to the given plan. + * + * Plans are considered similar if they have identical type and size. + * + * @param clusterPlan - Cluster plan with which to compare similarity. + * @param clusterPlans - Array from which to find similar cluster plans. + * + * @returns Array of similar cluster plans. + */ +const getSimilarPlans = ( + clusterPlan: LkePlanDescription, + clusterPlans: LkePlanDescription[] +) => { + return clusterPlans.filter((otherClusterPlan) => { + return ( + clusterPlan.type === otherClusterPlan.type && + clusterPlan.size === otherClusterPlan.size + ); + }); +}; diff --git a/packages/manager/cypress/support/api/lke.ts b/packages/manager/cypress/support/api/lke.ts index 14ed8339749..fa99f1eed71 100644 --- a/packages/manager/cypress/support/api/lke.ts +++ b/packages/manager/cypress/support/api/lke.ts @@ -1,29 +1,39 @@ import { - KubeNodePoolResponse, - KubernetesCluster, - PoolNodeResponse, deleteKubernetesCluster, getKubernetesClusters, getNodePools, } from '@linode/api-v4'; +import { DateTime } from 'luxon'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; -import { DateTime } from 'luxon'; import { isTestLabel } from './common'; +import type { + KubeNodePoolResponse, + KubernetesCluster, + PoolNodeResponse, +} from '@linode/api-v4'; +import type { LinodeTypeClass } from '@linode/api-v4/lib/linodes/types'; + /** * Describes an LKE plan as shown in Cloud Manager. */ export interface LkePlanDescription { - // / Plan size, GB. + /** Number of nodes in the plan. */ + nodeCount: number; + /** Name of the plan. */ + planName: string; + /** Plan size, GB. */ size: number; - - // / Label for tab containing the plan in creation screen. + /** Label for tab containing the plan in creation screen. */ tab: string; + /** Type of plan. */ + type: LinodeTypeClass; +} - // / Type of plan. - type: string; +export interface LkePlanDescriptionAPL extends LkePlanDescription { + disabled: boolean; } /* diff --git a/packages/manager/cypress/support/constants/dc-specific-pricing.ts b/packages/manager/cypress/support/constants/dc-specific-pricing.ts index 3843a35aceb..584fee1378f 100644 --- a/packages/manager/cypress/support/constants/dc-specific-pricing.ts +++ b/packages/manager/cypress/support/constants/dc-specific-pricing.ts @@ -3,7 +3,8 @@ */ import { linodeTypeFactory } from '@src/factories'; -import { LkePlanDescription } from 'support/api/lke'; + +import type { LkePlanDescription } from 'support/api/lke'; /** Notice shown to users when selecting a region with a different price structure. */ export const dcPricingRegionDifferenceNotice = @@ -127,9 +128,11 @@ export const dcPricingMockLinodeTypesForBackups = linodeTypeFactory.buildList( export const dcPricingLkeClusterPlans: LkePlanDescription[] = dcPricingMockLinodeTypes.map( (type) => { return { + nodeCount: 1, + planName: 'Linode 2 GB', size: parseInt(type.id.split('-')[2], 10), tab: 'Shared CPU', - type: 'Linode', + type: 'nanode', }; } ); diff --git a/packages/manager/cypress/support/constants/lke.ts b/packages/manager/cypress/support/constants/lke.ts index 1a4ca20aded..c6db3407abb 100644 --- a/packages/manager/cypress/support/constants/lke.ts +++ b/packages/manager/cypress/support/constants/lke.ts @@ -1,14 +1,3 @@ -import { LkePlanDescription } from 'support/api/lke'; - -/** - * Subset of LKE cluster plans as shown on Cloud Manager. - */ -export const lkeClusterPlans: LkePlanDescription[] = [ - { size: 4, tab: 'Dedicated CPU', type: 'Dedicated' }, - { size: 2, tab: 'Shared CPU', type: 'Linode' }, - { size: 4, tab: 'Shared CPU', type: 'Linode' }, -]; - /** * Kubernetes versions available for cluster creation via Cloud Manager. */ diff --git a/packages/manager/cypress/support/intercepts/betas.ts b/packages/manager/cypress/support/intercepts/betas.ts index 384961da39e..4874620ec77 100644 --- a/packages/manager/cypress/support/intercepts/betas.ts +++ b/packages/manager/cypress/support/intercepts/betas.ts @@ -6,7 +6,7 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; -import type { Beta } from '@linode/api-v4'; +import type { AccountBeta, Beta } from '@linode/api-v4'; /** * Intercepts GET request to fetch account betas (the ones the user has opted into) and mocks response. @@ -15,7 +15,9 @@ import type { Beta } from '@linode/api-v4'; * * @returns Cypress chainable. */ -export const mockGetAccountBetas = (betas: Beta[]): Cypress.Chainable => { +export const mockGetAccountBetas = ( + betas: AccountBeta[] +): Cypress.Chainable => { return cy.intercept( 'GET', apiMatcher('account/betas'), @@ -23,6 +25,21 @@ export const mockGetAccountBetas = (betas: Beta[]): Cypress.Chainable => { ); }; +/** + * Intercepts GET request to fetch a beta and mocks response. + * + * @param beta - Beta with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetAccountBeta = (beta: AccountBeta): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`account/betas/${beta.id}`), + makeResponse(beta) + ); +}; + /** * Intercepts GET request to fetch available betas (all betas available to the user). * diff --git a/packages/manager/cypress/support/intercepts/lke.ts b/packages/manager/cypress/support/intercepts/lke.ts index 5f646730a96..3f91e6a74a2 100644 --- a/packages/manager/cypress/support/intercepts/lke.ts +++ b/packages/manager/cypress/support/intercepts/lke.ts @@ -19,6 +19,7 @@ import type { KubernetesCluster, KubernetesControlPlaneACLPayload, KubernetesVersion, + PriceType, } from '@linode/api-v4'; /** @@ -455,3 +456,16 @@ export const mockUpdateControlPlaneACLError = ( makeErrorResponse(errorMessage, statusCode) ); }; + +/** + * Intercepts GET request for LKE cluster types and mocks the response + * + * @param types - LKE cluster types with which to mock response + * + * @returns Cypress chainable + */ +export const mockGetLKEClusterTypes = ( + types: PriceType[] +): Cypress.Chainable => { + return cy.intercept('GET', apiMatcher('lke/types*'), paginateResponse(types)); +}; diff --git a/packages/manager/src/factories/betas.ts b/packages/manager/src/factories/betas.ts index 80041895527..60b3c8c99c0 100644 --- a/packages/manager/src/factories/betas.ts +++ b/packages/manager/src/factories/betas.ts @@ -1,20 +1,23 @@ -import { Beta, AccountBeta } from '@linode/api-v4'; -import Factory from 'src/factories/factoryProxy'; import { DateTime } from 'luxon'; +import Factory from 'src/factories/factoryProxy'; + +import type { AccountBeta, Beta } from '@linode/api-v4'; + export const betaFactory = Factory.Sync.makeFactory({ + description: + 'Aliquam erat volutpat. Nunc eleifend leo vitae magna. In id erat non orci commodo lobortis. Proin neque massa, cursus ut, gravida ut, lobortis eget, lacus. Sed diam. Praesent fermentum tempor tellus. Nullam tempus. Mauris ac felis vel velit tristique imperdiet. Donec at pede. Etiam vel neque nec dui dignissim bibendum. Vivamus id enim. Phasellus neque orci, porta a, aliquet quis, semper a, massa. Phasellus purus. Pellentesque tristique imperdiet tortor. Nam euismod tellus id erat.', id: Factory.each((i) => `beta-${i}`), label: Factory.each((i) => `Beta ${i}`), started: DateTime.now().toISO(), - description: - 'Aliquam erat volutpat. Nunc eleifend leo vitae magna. In id erat non orci commodo lobortis. Proin neque massa, cursus ut, gravida ut, lobortis eget, lacus. Sed diam. Praesent fermentum tempor tellus. Nullam tempus. Mauris ac felis vel velit tristique imperdiet. Donec at pede. Etiam vel neque nec dui dignissim bibendum. Vivamus id enim. Phasellus neque orci, porta a, aliquet quis, semper a, massa. Phasellus purus. Pellentesque tristique imperdiet tortor. Nam euismod tellus id erat.', }); export const accountBetaFactory = Factory.Sync.makeFactory({ - id: Factory.each((i) => `beta-${i}`), - label: Factory.each((i) => `Account Beta ${i}`), - started: DateTime.now().toISO(), description: 'Aliquam erat volutpat. Nunc eleifend leo vitae magna. In id erat non orci commodo lobortis. Proin neque massa, cursus ut, gravida ut, lobortis eget, lacus. Sed diam. Praesent fermentum tempor tellus. Nullam tempus. Mauris ac felis vel velit tristique imperdiet. Donec at pede. Etiam vel neque nec dui dignissim bibendum. Vivamus id enim. Phasellus neque orci, porta a, aliquet quis, semper a, massa. Phasellus purus. Pellentesque tristique imperdiet tortor. Nam euismod tellus id erat.', + ended: null, enrolled: DateTime.now().toISO(), + id: Factory.each((i) => `beta-${i}`), + label: Factory.each((i) => `Account Beta ${i}`), + started: DateTime.now().toISO(), }); diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index a8cfd57cd1d..715007bb1ef 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -235,12 +235,11 @@ export const databaseFactory = Factory.Sync.makeFactory({ }); export const databaseBackupFactory = Factory.Sync.makeFactory({ - created: Factory.each(() => - randomDate( - new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), - new Date() - ).toISOString() - ), + created: Factory.each(() => { + const now = new Date(); + const tenDaysAgo = new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000); + return randomDate(tenDaysAgo, now).toISOString(); + }), id: Factory.each((i) => i), label: Factory.each(() => `backup-${v4()}`), type: pickRandom(['snapshot', 'auto']), diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx index d0f4e0c135d..20d0f5557fc 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ApplicationPlatform.tsx @@ -36,7 +36,7 @@ export const ApplicationPlatform = (props: APLProps) => { }; return ( - + ({ '&&.MuiFormLabel-root.Mui-focused': { @@ -48,19 +48,24 @@ export const ApplicationPlatform = (props: APLProps) => { })} > - Akamai App Platform + Akamai App Platform handleChange(e)}> } label={Yes, enable Akamai App Platform.} - control={} name="yes" value="yes" /> - } label="No" name="no" value="no" /> + } + label="No" + name="no" + value="no" + /> ); diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/HAControlPlane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/HAControlPlane.tsx index b886bf2861b..794921ef8ec 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/HAControlPlane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/HAControlPlane.tsx @@ -117,7 +117,7 @@ export const HAControlPlane = (props: HAControlPlaneProps) => { } + control={} label="No" name="no" value="no"