diff --git a/packages/manager/.changeset/pr-13410-changed-1772435694557.md b/packages/manager/.changeset/pr-13410-changed-1772435694557.md new file mode 100644 index 00000000000..dcd3b8c1141 --- /dev/null +++ b/packages/manager/.changeset/pr-13410-changed-1772435694557.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Make firewall selection mandatory while creating linode and its interfaces ([#13410](https://github.com/linode/manager/pull/13410)) diff --git a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts index c89c6cbc6c9..9aaa67132c6 100644 --- a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts +++ b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts @@ -1,9 +1,12 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; +import { firewallFactory } from '@src/factories'; import { mockGetAccountAgreements } from 'support/intercepts/account'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { randomLabel, randomString } from 'support/util/random'; +import { linodeCreatePage } from 'support/ui/pages'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import type { Region } from '@linode/api-v4'; @@ -100,6 +103,11 @@ describe('GDPR agreement', () => { }); it('needs the agreement checked to submit the form', () => { + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockGetRegions(mockRegions).as('getRegions'); mockGetAccountAgreements({ billing_agreement: false, @@ -127,6 +135,12 @@ describe('GDPR agreement', () => { cy.findByLabelText('Root Password').type(rootpass); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); + cy.get('[data-testid="eu-agreement-checkbox"]') .as('euAgreement') .scrollIntoView(); diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts index f979b0cd28a..b41149900de 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts @@ -6,6 +6,7 @@ import 'cypress-file-upload'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetDomains } from 'support/intercepts/domains'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinodeAccountLimitError, mockGetLinodeDetails, @@ -31,6 +32,7 @@ import { } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; import { accountFactory, domainFactory, @@ -378,6 +380,10 @@ describe('open support tickets', () => { planLabel: 'Nanode 1 GB', planId: 'g6-nanode-1', }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); const mockLinode = linodeFactory.build(); @@ -393,6 +399,7 @@ describe('open support tickets', () => { mockGetSupportTicket(mockAccountLimitTicket); mockGetSupportTicketReplies(mockAccountLimitTicket.id, []); mockGetLinodes([mockLinode]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); @@ -401,6 +408,8 @@ describe('open support tickets', () => { linodeCreatePage.selectRegionById(mockRegion.id); linodeCreatePage.selectPlan(mockPlan.planType, mockPlan.planLabel); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Attempt to create Linode and confirm mocked account limit error with support link is present. ui.button diff --git a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts index 40aef7bf9ed..3206e9ed69e 100644 --- a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts @@ -1,7 +1,9 @@ import { linodeFactory } from '@linode/utilities'; -import { imageFactory } from '@src/factories'; +import { firewallFactory, imageFactory } from '@src/factories'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockGetAllImages } from 'support/intercepts/images'; import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { apiMatcher } from 'support/util/intercepts'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -20,8 +22,14 @@ const mockImage = imageFactory.build({ label: randomLabel(), }); +const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), +}); + const createLinodeWithImageMock = (url: string, preselectedImage: boolean) => { mockGetAllImages([mockImage]).as('mockImage'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.intercept('POST', apiMatcher('linode/instances'), (req) => { req.reply({ @@ -52,6 +60,11 @@ const createLinodeWithImageMock = (url: string, preselectedImage: boolean) => { cy.findByText('Shared CPU').click(); cy.get('[id="g6-nanode-1"][type="radio"]').click(); cy.get('[id="root-password"]').type(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); ui.button .findByTitle('Create Linode') diff --git a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts index 43325319275..329debdf69c 100644 --- a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts @@ -2,15 +2,21 @@ import { regionAvailabilityFactory, regionFactory } from '@linode/utilities'; import { mockGetAccountSettings } from 'support/intercepts/account'; import { mockGetAlertDefinition } from 'support/intercepts/cloudpulse'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { interceptCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegionAvailability, mockGetRegions, } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { randomLabel, randomString } from 'support/util/random'; +import { linodeCreatePage } from 'support/ui/pages'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; -import { accountSettingsFactory, alertFactory } from 'src/factories'; +import { + accountSettingsFactory, + alertFactory, + firewallFactory, +} from 'src/factories'; import { ALERTS_BETA_MODE_BANNER_TEXT, ALERTS_BETA_MODE_BUTTON_TEXT, @@ -18,6 +24,11 @@ import { ALERTS_LEGACY_MODE_BUTTON_TEXT, } from 'src/features/Linodes/constants'; +const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), +}); + describe('Create flow when beta alerts enabled by region and feature flag', function () { beforeEach(() => { const mockEnabledRegion = regionFactory.build({ @@ -54,6 +65,7 @@ describe('Create flow when beta alerts enabled by region and feature flag', func interfaces_for_new_linodes: 'legacy_config_default_but_linode_allowed', }); mockGetAccountSettings(mockInitialAccountSettings).as('getSettings'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); }); it('Alerts panel becomes visible after switching to region w/ alerts enabled', function () { @@ -88,6 +100,11 @@ describe('Create flow when beta alerts enabled by region and feature flag', func const enabledRegion = this.mockRegions[0]; mockGetRegionAvailability(enabledRegion.id, []).as('getRegionAvailability'); ui.regionSelect.find().type(`${enabledRegion.label}{enter}`); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // legacy alerts panel appears cy.wait('@getRegionAvailability'); @@ -208,6 +225,11 @@ describe('Create flow when beta alerts enabled by region and feature flag', func const enabledRegion = this.mockRegions[0]; mockGetRegionAvailability(enabledRegion.id, []).as('getRegionAvailability'); ui.regionSelect.find().type(`${enabledRegion.label}{enter}`); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // legacy alerts panel appears cy.wait('@getRegionAvailability'); @@ -437,6 +459,11 @@ describe('Create flow when beta alerts enabled by region and feature flag', func 'getRegionAvailability' ); ui.regionSelect.find().type(`${disabledRegion.label}{enter}`); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); cy.wait('@getRegionAvailability'); // enter plan and password form fields to enable "View Code Snippets" button diff --git a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts index 434d839b5ef..c611bfd31d7 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -20,6 +20,7 @@ import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { interceptEvents } from 'support/intercepts/events'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { interceptCloneLinode, mockCloneLinode, @@ -44,6 +45,8 @@ import { } from 'support/util/random'; import { chooseRegion, extendRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; + import type { Linode } from '@linode/api-v4'; /** @@ -195,8 +198,13 @@ describe('clone linode', () => { id: mockLinode.id + 1, label: newLinodeLabel, }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); mockGetVLANs([mockVlan]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); mockGetLinodeVolumes(clonedLinode.id, [mockVolume]).as('getLinodeVolumes'); @@ -229,6 +237,8 @@ describe('clone linode', () => { .type(mockVlan.cidr_block); }); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Confirm that VLAN attachment is listed in summary, then create Linode. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-blackwell.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-blackwell.spec.ts index 93e7ef5fc69..035c51d9169 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-blackwell.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-blackwell.spec.ts @@ -5,6 +5,7 @@ import { regionFactory, } from '@linode/utilities'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeTypes, @@ -14,7 +15,11 @@ import { mockGetRegions, } from 'support/intercepts/regions'; import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; +import { randomNumber } from 'support/util/random'; + +import { firewallFactory } from 'src/factories'; const mockEnabledRegion = regionFactory.build({ id: 'us-east', @@ -37,10 +42,15 @@ const mockBlackwellLinodeTypes = new Array(4).fill(null).map((_, index) => const selectedBlackwell = mockBlackwellLinodeTypes[0]; describe('smoketest for Nvidia blackwell GPUs in linodes/create page', () => { + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); beforeEach(() => { mockGetRegions([mockEnabledRegion, mockDisabledRegion]).as('getRegions'); mockGetLinodeTypes(mockBlackwellLinodeTypes).as('getLinodeTypes'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); }); /* @@ -120,6 +130,11 @@ describe('smoketest for Nvidia blackwell GPUs in linodes/create page', () => { cy.findByLabelText('Linode Label').type(newLinodeLabel); cy.get('[type="password"]').should('be.visible').scrollIntoView(); cy.get('[id="root-password"]').type(randomString(12)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); cy.scrollTo('bottom'); const mockLinode = linodeFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts index df59f00ebe3..77d3236aaca 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts @@ -1,5 +1,6 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegionAvailability, @@ -8,6 +9,9 @@ import { import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; +import { randomNumber } from 'support/util/random'; + +import { firewallFactory } from 'src/factories'; describe('Create Linode in a Core Region', () => { /* @@ -30,6 +34,10 @@ describe('Create Linode in a Core Region', () => { region: mockRegion1.id, }); const rootPass = randomString(32); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); mockAppendFeatureFlags({ gecko2: { @@ -39,6 +47,7 @@ describe('Create Linode in a Core Region', () => { }).as('getFeatureFlags'); mockGetRegions(mockRegions).as('getRegions'); mockGetRegionAvailability(mockRegion1.id, []).as('getRegionAvailability'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -55,6 +64,11 @@ describe('Create Linode in a Core Region', () => { linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.setRootPassword(rootPass); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); ui.button .findByTitle('Create Linode') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts index 8f1ccfd6833..be42ffc1206 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts @@ -4,6 +4,7 @@ import { regionFactory, } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeTypes, @@ -14,9 +15,11 @@ import { } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; + import type { Region } from '@linode/api-v4'; describe('Create Linode in Distributed Region', () => { @@ -42,6 +45,10 @@ describe('Create Linode in Distributed Region', () => { label: randomLabel(), region: mockRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); const rootPass = randomString(32); mockAppendFeatureFlags({ @@ -51,6 +58,7 @@ describe('Create Linode in Distributed Region', () => { }, }).as('getFeatureFlags'); mockGetRegions([mockRegion]).as('getRegions'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockGetLinodeTypes(mockLinodeTypes).as('getLinodeTypes'); mockGetRegionAvailability(mockRegion.id, []).as('getRegionAvailability'); mockCreateLinode(mockLinode).as('createLinode'); @@ -75,6 +83,12 @@ describe('Create Linode in Distributed Region', () => { .click(); }); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); + ui.button .findByTitle('Create Linode') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts index ffc2939ce95..c12df22e5e3 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts @@ -4,12 +4,15 @@ import { linodeFactory } from '@linode/utilities'; import { MOBILE_VIEWPORTS } from 'support/constants/environment'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; + describe('Linode create mobile smoke', () => { MOBILE_VIEWPORTS.forEach((viewport) => { /* @@ -23,7 +26,11 @@ describe('Linode create mobile smoke', () => { label: randomLabel(), region: mockLinodeRegion.id, }); - + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.viewport(viewport.width, viewport.height); @@ -34,6 +41,11 @@ describe('Linode create mobile smoke', () => { linodeCreatePage.selectPlanCard('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setLabel(mockLinode.label); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts index b74046af2fc..358c01392a1 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts @@ -3,11 +3,14 @@ */ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; + describe('Create Linode flow to validate code snippet modal', () => { beforeEach(() => { mockAppendFeatureFlags({ @@ -24,6 +27,11 @@ describe('Create Linode flow to validate code snippet modal', () => { const mockLinodeRegion = chooseRegion({ capabilities: ['Linodes'], }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); // Set Linode label, distribution, plan type, password, etc. @@ -32,6 +40,8 @@ describe('Create Linode flow to validate code snippet modal', () => { linodeCreatePage.selectRegionById(mockLinodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(rootPass); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // View Code Snippets and confirm it's provisioned as expected. ui.button diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts index 7f0e8989401..256aecd29e3 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts @@ -1,13 +1,14 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccountSettings } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; -import { accountSettingsFactory } from 'src/factories'; +import { accountSettingsFactory, firewallFactory } from 'src/factories'; const mockEnabledRegion = regionFactory.build({ capabilities: ['Linodes', 'Maintenance Policy'], }); @@ -35,7 +36,12 @@ describe('vmHostMaintenance feature flag', () => { label: randomLabel(), region: mockEnabledRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); mockCreateLinode(mockLinode).as('createLinode'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); cy.wait(['@getAccountSettings', '@getFeatureFlags', '@getRegions']); @@ -82,6 +88,8 @@ describe('vmHostMaintenance feature flag', () => { planLabel: 'Nanode 1 GB', }; linodeCreatePage.selectPlan(mockPlan.planType, mockPlan.planLabel); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); cy.scrollTo('bottom'); ui.button .findByTitle('View Code Snippets') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts index 3dbdf20a47f..c5ec7562dd2 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts @@ -1,4 +1,5 @@ import { linodeFactory } from '@linode/utilities'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeDetails, @@ -8,7 +9,13 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { firewallFactory } from 'src/factories'; + describe('Create Linode with Add-ons', () => { + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); /* * - Confirms UI flow to create a Linode with backups using mock API data. * - Confirms that backups is reflected in create summary section. @@ -25,6 +32,7 @@ describe('Create Linode with Add-ons', () => { mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); @@ -33,6 +41,11 @@ describe('Create Linode with Add-ons', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); linodeCreatePage.checkBackups(); linodeCreatePage.checkEUAgreements(); @@ -78,6 +91,7 @@ describe('Create Linode with Add-ons', () => { mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); @@ -86,8 +100,15 @@ describe('Create Linode with Add-ons', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); linodeCreatePage.checkEUAgreements(); linodeCreatePage.selectInterfaceGeneration('legacy_config'); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); linodeCreatePage.checkPrivateIPs(); // Confirm Private IP assignment indicator is shown in Linode summary. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts index 093b89bd120..351d0e56400 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts @@ -3,9 +3,10 @@ import { linodeTypeFactory, regionFactory, } from '@linode/utilities'; -import { accountFactory } from '@src/factories'; +import { accountFactory, firewallFactory } from '@src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeTypes, @@ -17,7 +18,7 @@ import { import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { makeFeatureFlagData } from 'support/util/feature-flags'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; import { @@ -156,6 +157,10 @@ describe('Create Linode with Disk Encryption', () => { label: randomLabel(), region: distributedRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); mockAppendFeatureFlags({ gecko2: { @@ -167,6 +172,7 @@ describe('Create Linode with Disk Encryption', () => { mockGetLinodeTypes([mockLinodeType]); mockGetRegionAvailability(distributedRegion.id, []); mockCreateLinode(mockLinode).as('createLinode'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); cy.get('[data-qa-linode-region]').within(() => { @@ -185,7 +191,11 @@ describe('Create Linode with Disk Encryption', () => { linodeCreatePage.setLabel(mockLinode.label); linodeCreatePage.setRootPassword(randomString(32)); - + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Select mock Nanode plan type. cy.get('[data-qa-plan-row="Nanode 1 GB"]').click(); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts index b87f8dea920..b3aa9d9f892 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts @@ -424,17 +424,8 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Switch to legacy Config Interfaces linodeCreatePage.selectLegacyConfigInterfacesType(); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. - cy.findByLabelText('Firewall').should('be.visible'); - cy.get('[data-qa-autocomplete="Firewall"]').within(() => { - cy.get('[data-testid="textfield-input"]').click(); - cy.focused().type(`${mockFirewall.label}`); - }); - - ui.autocompletePopper - .findByTitle(mockFirewall.label) - .should('be.visible') - .click(); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); // Confirm Firewall assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); @@ -494,17 +485,10 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. - cy.findByLabelText('Public Interface Firewall').should('be.visible'); - cy.get('[data-qa-autocomplete="Public Interface Firewall"]').within(() => { - cy.get('[data-testid="textfield-input"]').click(); - cy.focused().type(`${mockFirewall.label}`); - }); - - ui.autocompletePopper - .findByTitle(mockFirewall.label) - .should('be.visible') - .click(); + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Confirm Firewall assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); @@ -593,17 +577,7 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { `Firewall ${mockFirewall.label} successfully created` ); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. - cy.findByLabelText('Firewall').should('be.visible'); - cy.get('[data-qa-autocomplete="Firewall"]').within(() => { - cy.get('[data-testid="textfield-input"]').click(); - cy.focused().type(`${mockFirewall.label}`); - }); - - ui.autocompletePopper - .findByTitle(mockFirewall.label) - .should('be.visible') - .click(); + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); // Confirm Firewall assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); @@ -688,17 +662,10 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { `Firewall ${mockFirewall.label} successfully created` ); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. - cy.findByLabelText('Public Interface Firewall').should('be.visible'); - cy.get('[data-qa-autocomplete="Public Interface Firewall"]').within(() => { - cy.get('[data-testid="textfield-input"]').click(); - cy.focused().type(`${mockFirewall.label}`); - }); - - ui.autocompletePopper - .findByTitle(mockFirewall.label) - .should('be.visible') - .click(); + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Confirm Firewall assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts index fac8dd00c69..b15ecf35878 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts @@ -1,5 +1,6 @@ import { linodeFactory, sshKeyFactory } from '@linode/utilities'; import { mockGetUser, mockGetUsers } from 'support/intercepts/account'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockCreateSSHKey } from 'support/intercepts/profile'; import { ui } from 'support/ui'; @@ -7,9 +8,13 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { accountUserFactory } from 'src/factories'; +import { accountUserFactory, firewallFactory } from 'src/factories'; describe('Create Linode with SSH Key', () => { + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); /* * - Confirms UI flow when creating a Linode with an authorized SSH key. * - Confirms that existing SSH keys are listed on page and can be selected. @@ -34,6 +39,7 @@ describe('Create Linode with SSH Key', () => { mockGetUsers([mockUser]); mockGetUser(mockUser); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -43,6 +49,11 @@ describe('Create Linode with SSH Key', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Confirm that SSH key is listed, then select it. cy.findByText(mockSshKey.label).scrollIntoView(); @@ -101,6 +112,7 @@ describe('Create Linode with SSH Key', () => { mockGetUsers([mockUser]); mockCreateLinode(mockLinode).as('createLinode'); mockCreateSSHKey(mockSshKey).as('createSSHKey'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); @@ -109,6 +121,11 @@ describe('Create Linode with SSH Key', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Confirm that no SSH keys are listed for the mocked user. cy.findByText(mockUser.username).scrollIntoView(); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index 9fb53cba649..8330b471b9d 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -1,4 +1,5 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { mockCreateLinode, @@ -10,7 +11,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory } from 'src/factories'; +import { firewallFactory, imageFactory } from 'src/factories'; describe('Create Linode with user data', () => { /* @@ -26,10 +27,15 @@ describe('Create Linode with user data', () => { label: randomLabel(), region: linodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); const userDataFixturePath = 'user-data/user-data-config-basic.yml'; mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin('/linodes/create'); @@ -40,6 +46,11 @@ describe('Create Linode with user data', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); // Expand "Add User Data" accordion and enter user data config. ui.accordionHeading diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index 1ed9356b23b..f55e562ee2b 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -4,6 +4,7 @@ import { mockGetAccountSettings, } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegion, mockGetRegions } from 'support/intercepts/regions'; import { mockGetVLANs } from 'support/intercepts/vlans'; @@ -21,6 +22,7 @@ import { chooseRegion } from 'support/util/regions'; import { accountFactory, accountSettingsFactory, + firewallFactory, VLANFactory, } from 'src/factories'; @@ -56,7 +58,13 @@ describe('Create Linode with VLANs (Legacy)', () => { region: mockLinodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVLANs([mockVlan]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -89,6 +97,9 @@ describe('Create Linode with VLANs (Legacy)', () => { .type(mockVlan.cidr_block); }); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); + // Confirm that VLAN attachment is listed in summary, then create Linode. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -146,7 +157,13 @@ describe('Create Linode with VLANs (Legacy)', () => { region: mockLinodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVLANs([]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -175,6 +192,9 @@ describe('Create Linode with VLANs (Legacy)', () => { .click(); }); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); + // Confirm that VLAN attachment is listed in summary, then create Linode. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -305,7 +325,13 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { region: mockLinodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVLANs([mockVlan]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -338,6 +364,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { .should('be.enabled') .type(mockVlan.cidr_block); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); + // Confirm that VLAN attachment is listed in summary, then create Linode. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -471,7 +500,13 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { region: mockLinodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVLANs([]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -505,6 +540,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { .should('be.enabled') .type(mockVlan.cidr_block); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); + // Confirm that VLAN attachment is listed in summary, then create Linode. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index 0f9fa5ab253..b1a97b5da75 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -9,6 +9,7 @@ import { } from 'support/intercepts/account'; import { mockGetLinodeConfig } from 'support/intercepts/configs'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeDetails, @@ -36,6 +37,7 @@ import { chooseRegion } from 'support/util/regions'; import { accountFactory, accountSettingsFactory, + firewallFactory, linodeConfigFactory, subnetFactory, vpcFactory, @@ -106,8 +108,14 @@ describe('Create Linode with VPCs (Legacy)', () => { ], }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVPCs([mockVPC]).as('getVPCs'); mockGetVPC(mockVPC).as('getVPC'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode); @@ -137,6 +145,9 @@ describe('Create Linode with VPCs (Legacy)', () => { `${mockSubnet.label} (${mockSubnet.ipv4})` ); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); + // Confirm VPC assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -240,7 +251,13 @@ describe('Create Linode with VPCs (Legacy)', () => { ], }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetVPCs([]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -293,6 +310,9 @@ describe('Create Linode with VPCs (Legacy)', () => { cy.findByLabelText('Clear').click(); }); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); + // Try to submit the form without a subnet selected ui.button .findByTitle('Create Linode') @@ -460,6 +480,11 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { ], }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockGetVPCs([mockVPC]).as('getVPCs'); mockGetVPC(mockVPC).as('getVPC'); mockCreateLinode(mockLinode).as('createLinode'); @@ -500,6 +525,9 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { `${mockSubnet.label} (${mockSubnet.ipv4})` ); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); + // Confirm VPC assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -572,6 +600,11 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { region: linodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ active: true, primary: true, @@ -601,6 +634,7 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { mockGetVPCs([mockVPC]).as('getVPCs'); mockGetVPC(mockVPC).as('getVPC'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); mockGetLinodeDetails(mockLinode.id, mockLinode); @@ -636,6 +670,12 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { `${mockSubnet.label} (${mockSubnet.ipv4})` ); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'VPC Interface Firewall' + ); + // Confirm VPC assignment indicator is shown in Linode summary. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); cy.get('[data-qa-linode-create-summary]').within(() => { @@ -737,6 +777,13 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { ], }; + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + + mockGetFirewalls([mockFirewall]).as('getFirewalls'); + mockGetVPCs([]); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -755,6 +802,8 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Select VPC card linodeCreatePage.selectInterface('vpc'); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall(mockFirewall.label, 'Firewall'); cy.findByText('Create VPC').should('be.visible').click(); @@ -896,6 +945,11 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { region: linodeRegion.id, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); + const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ active: true, primary: true, @@ -924,6 +978,7 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { }; mockGetVPCs([]); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -1009,6 +1064,12 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { .should('be.visible') .click(); + // Select a firewall for the VPC interface + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'VPC Interface Firewall' + ); + // Create Linode and confirm contents of outgoing API request payload. ui.button .findByTitle('Create Linode') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index eb6eec87f92..dd43845b810 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -13,6 +13,7 @@ import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { interceptCreateLinode, mockCreateLinode, @@ -31,10 +32,17 @@ import { cleanUp } from 'support/util/cleanup'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { accountFactory, accountUserFactory } from 'src/factories'; +import { + accountFactory, + accountUserFactory, + firewallFactory, +} from 'src/factories'; let username: string; - +const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), +}); authenticate(); describe('Create Linode', () => { before(() => { @@ -54,6 +62,12 @@ describe('Create Linode', () => { describe('End-to-end', () => { // Run an end-to-end test to create a basic Linode for each plan type described below. describe('By plan type', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + linodeInterfaces: { enabled: true }, + }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); + }); [ { planId: 'g6-nanode-1', @@ -81,6 +95,7 @@ describe('Create Linode', () => { const linodeRegion = chooseRegion({ capabilities: ['Linodes', 'Vlans'], }); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); const linodeLabel = randomLabel(); @@ -108,6 +123,11 @@ describe('Create Linode', () => { planConfig.planLabel ); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall( + 'No firewall - traffic is unprotected (not recommended)', + 'Public Interface Firewall' + ); // Confirm information in summary is shown as expected. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); @@ -227,6 +247,7 @@ describe('Create Linode', () => { }).as('getFeatureFlags'); mockGetRegions(mockRegions).as('getRegions'); mockGetLinodeTypes([...mockAcceleratedType]).as('getLinodeTypes'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinode(mockLinode).as('createLinode'); cy.visitWithLogin('/linodes/create'); @@ -243,6 +264,8 @@ describe('Create Linode', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Accelerated', mockAcceleratedType[0].label); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Confirm information in summary is shown as expected. cy.get('[data-qa-linode-create-summary]').scrollIntoView(); @@ -297,6 +320,7 @@ describe('Create Linode', () => { const createLinodeErrorMessage = 'An error has occurred during Linode creation flow'; + mockGetFirewalls([mockFirewall]).as('getFirewalls'); mockCreateLinodeError(createLinodeErrorMessage).as('createLinodeError'); cy.visitWithLogin('/linodes/create'); @@ -306,6 +330,8 @@ describe('Create Linode', () => { linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Create Linode by clicking the button. ui.button diff --git a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts index 8c1e15ab9bd..f1779baa308 100644 --- a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts @@ -1,4 +1,5 @@ import { linodeFactory } from '@linode/utilities'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockGetAllImages } from 'support/intercepts/images'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { @@ -7,11 +8,12 @@ import { mockGetStackScripts, } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { getRandomOCAId } from 'support/util/one-click-apps'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory } from 'src/factories'; +import { firewallFactory, imageFactory } from 'src/factories'; import { stackScriptFactory } from 'src/factories/stackscripts'; import { getMarketplaceAppLabel } from 'src/features/Linodes/LinodeCreate/Tabs/Marketplace/utilities'; import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; @@ -166,10 +168,15 @@ describe('OneClick Apps (OCA)', () => { const linode = linodeFactory.build({ label: linodeLabel, }); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); mockGetAllImages(images); mockGetStackScripts([stackscript]).as('getStackScripts'); mockGetStackScript(stackscript.id, stackscript); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); cy.visitWithLogin(`/linodes/create/marketplace`); @@ -242,6 +249,12 @@ describe('OneClick Apps (OCA)', () => { // Create the Linode mockCreateLinode(linode).as('createLinode'); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); + ui.button .findByTitle('Create Linode') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts index dfd189af374..f592606a2e1 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts @@ -1,5 +1,6 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { mockCreateLinode, mockGetLinodeDetails, @@ -11,10 +12,14 @@ import { import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui/'; import { linodeCreatePage } from 'support/ui/pages'; -import { randomNumber, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import { accountFactory, placementGroupFactory } from 'src/factories'; +import { + accountFactory, + firewallFactory, + placementGroupFactory, +} from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; const mockAccount = accountFactory.build(); @@ -37,12 +42,18 @@ const mockDallasRegion = extendRegion( }) ); +const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), +}); + const mockRegions = [mockNewarkRegion, mockDallasRegion]; describe('Linode create flow with Placement Group', () => { beforeEach(() => { mockGetAccount(mockAccount); mockGetRegions(mockRegions).as('getRegions'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); }); /* @@ -90,6 +101,8 @@ describe('Linode create flow with Placement Group', () => { // Choose plan cy.findByText('Shared CPU').click(); cy.get('[id="g6-nanode-1"]').click(); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Choose Placement Group // No Placement Group available @@ -241,6 +254,8 @@ describe('Linode create flow with Placement Group', () => { linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); linodeCreatePage.setLabel(mockLinode.label); + // Select a firewall + linodeCreatePage.selectFirewall(mockFirewall.label, 'Assign Firewall'); // Confirm that mocked Placement Group is shown in the Autocomplete, and then select it. cy.findByText( diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 2d793df73f5..903f449da50 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -2,6 +2,7 @@ import { createImage, getLinodeDisks, resizeLinodeDisk } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptGetAccountAvailability } from 'support/intercepts/account'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { interceptGetAllImages } from 'support/intercepts/images'; import { interceptCreateLinode } from 'support/intercepts/linodes'; import { @@ -9,6 +10,7 @@ import { interceptGetStackScripts, } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { cleanUp } from 'support/util/cleanup'; import { chooseImage } from 'support/util/images'; @@ -18,10 +20,16 @@ import { pollLinodeDiskSize, pollLinodeStatus, } from 'support/util/polling'; -import { randomLabel, randomPhrase, randomString } from 'support/util/random'; +import { + randomLabel, + randomNumber, + randomPhrase, + randomString, +} from 'support/util/random'; import { chooseRegion, getRegionByLabel } from 'support/util/regions'; import { getFilteredImagesForImageSelect } from 'src/components/ImageSelect/utilities'; +import { firewallFactory } from 'src/factories'; import type { Image } from '@linode/api-v4'; @@ -179,6 +187,11 @@ const createLinodeAndImage = async () => { return image; }; +const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), +}); + authenticate(); describe('Create stackscripts', () => { before(() => { @@ -186,6 +199,7 @@ describe('Create stackscripts', () => { }); beforeEach(() => { cy.tag('method:e2e', 'purpose:dcTesting'); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); }); /* @@ -288,6 +302,11 @@ describe('Create stackscripts', () => { cy.findByLabelText('Example Title').should('be.visible').click(); cy.focused().type('{selectall}{backspace}'); cy.focused().type(randomString(12)); + // Select a firewall + linodeCreatePage.selectFirewall( + 'No firewall - traffic is unprotected (not recommended)', + 'Public Interface Firewall' + ); ui.button .findByTitle('Create Linode') @@ -389,6 +408,11 @@ describe('Create stackscripts', () => { cy.findByText(privateImage.label).as('qaPrivateImage').scrollIntoView(); cy.get('@qaPrivateImage').should('be.visible').click(); + // Select a firewall + linodeCreatePage.selectFirewall( + 'No firewall - traffic is unprotected (not recommended)', + 'Public Interface Firewall' + ); interceptCreateLinode().as('createLinode'); fillOutLinodeForm( linodeLabel, diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts index 7e137192517..e87e040420b 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts @@ -1,5 +1,6 @@ import { getProfile } from '@linode/api-v4'; import { authenticate } from 'support/api/authentication'; +import { mockGetFirewalls } from 'support/intercepts/firewalls'; import { interceptCreateLinode } from 'support/intercepts/linodes'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { @@ -8,11 +9,12 @@ import { mockGetStackScripts, } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { cleanUp } from 'support/util/cleanup'; -import { randomLabel, randomString } from 'support/util/random'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { stackScriptFactory } from 'src/factories'; +import { firewallFactory, stackScriptFactory } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; import type { Profile, StackScript } from '@linode/api-v4'; @@ -284,11 +286,16 @@ describe('Community Stackscripts integration tests', () => { const image = 'AlmaLinux 9'; const region = chooseRegion({ capabilities: ['Linodes', 'Vlans'] }); const linodeLabel = randomLabel(); + const mockFirewall = firewallFactory.build({ + id: randomNumber(), + label: randomLabel(), + }); // Ensure that the Primary Nav is open mockGetUserPreferences({ desktop_sidebar_open: false }).as( 'getPreferences' ); + mockGetFirewalls([mockFirewall]).as('getFirewalls'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin('/stackscripts/community'); cy.wait(['@getStackScripts', '@getPreferences']); @@ -392,6 +399,12 @@ describe('Community Stackscripts integration tests', () => { cy.get('[data-qa-radio]').click({ force: true }); }); + // Select a firewall + linodeCreatePage.selectFirewall( + mockFirewall.label, + 'Public Interface Firewall' + ); + // Input root password // Weak or fair root password cannot rebuild the linode cy.get('[id="root-password"]').clear(); diff --git a/packages/manager/cypress/support/ui/pages/linode-create-page.ts b/packages/manager/cypress/support/ui/pages/linode-create-page.ts index 47e370d7ffd..b3f9741a81f 100644 --- a/packages/manager/cypress/support/ui/pages/linode-create-page.ts +++ b/packages/manager/cypress/support/ui/pages/linode-create-page.ts @@ -157,4 +157,30 @@ export const linodeCreatePage = { selectInterface: (type: 'public' | 'vlan' | 'vpc') => { cy.get(`[data-qa-interface-type-option="${type}"]`).click(); }, + + /** + * Selects a firewall from the firewall dropdown. + * + * @param firewallLabel - Label of the firewall to select. + * @param interfaceType - Optional interface type for the firewall dropdown label (e.g., 'Public Interface Firewall', 'VPC Interface Firewall'). + */ + selectFirewall: ( + firewallLabel: string, + dropdownLabel: + | 'Assign Firewall' + | 'Firewall' + | 'Public Interface Firewall' + | 'VPC Interface Firewall' + ) => { + cy.findByLabelText(dropdownLabel).should('be.visible'); + cy.get(`[data-qa-autocomplete="${dropdownLabel}"]`).within(() => { + cy.get('[data-testid="textfield-input"]').click(); + cy.focused().type(firewallLabel); + }); + + ui.autocompletePopper + .findByTitle(firewallLabel) + .should('be.visible') + .click(); + }, }; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index ba990a3c94d..95494c39820 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -135,7 +135,7 @@ export const createTestLinode = async ( const resolvedCreatePayload = { ...createLinodeRequestFactory.build({ interface_generation: 'legacy_config', - firewall_id: null, + firewall_id: -1, booted: false, image: 'linode/ubuntu24.04', label: randomLabel(), diff --git a/packages/manager/src/features/Account/DefaultFirewalls.tsx b/packages/manager/src/features/Account/DefaultFirewalls.tsx index 84af676e298..5197c6d3038 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.tsx @@ -126,6 +126,7 @@ export const DefaultFirewalls = () => { label="Configuration Profile Interfaces Firewall" onChange={(e, firewall) => field.onChange(firewall.id)} placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + showNoFirewallOption={false} value={field.value} /> )} @@ -142,6 +143,7 @@ export const DefaultFirewalls = () => { label="Linode Interfaces - Public Interface Firewall" onChange={(e, firewall) => field.onChange(firewall.id)} placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + showNoFirewallOption={false} value={field.value} /> )} @@ -158,6 +160,7 @@ export const DefaultFirewalls = () => { label="Linode Interfaces - VPC Interface Firewall" onChange={(e, firewall) => field.onChange(firewall.id)} placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + showNoFirewallOption={false} value={field.value} /> )} @@ -177,6 +180,7 @@ export const DefaultFirewalls = () => { label="NodeBalancers Firewall" onChange={(e, firewall) => field.onChange(firewall.id)} placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + showNoFirewallOption={false} value={field.value} /> )} diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx index 5489c7cff9c..3b038dd20b8 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx @@ -8,6 +8,10 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { FirewallSelect } from './FirewallSelect'; +const NO_FIREWALL_ID = -1; +const NO_FIREWALL_LABEL = + 'No firewall - traffic is unprotected (not recommended)'; + describe('FirewallSelect', () => { it('renders a default label', () => { const { getByText } = renderWithTheme(); @@ -50,4 +54,75 @@ describe('FirewallSelect', () => { expect(getByText(firewall.label)).toBeVisible(); } }); + + it('renders "No firewall" option in the dropdown by default', async () => { + const firewalls = firewallFactory.buildList(2); + + server.use( + http.get('*/v4/networking/firewalls', () => { + return HttpResponse.json(makeResourcePage(firewalls)); + }) + ); + + const { getByLabelText, getByText } = renderWithTheme( + + ); + + await userEvent.click(getByLabelText('Firewall')); + + expect(getByText(NO_FIREWALL_LABEL)).toBeVisible(); + }); + + it('does not render "No firewall" option when showNoFirewallOption is false', async () => { + const firewalls = firewallFactory.buildList(2); + + server.use( + http.get('*/v4/networking/firewalls', () => { + return HttpResponse.json(makeResourcePage(firewalls)); + }) + ); + + const { getByLabelText, queryByText } = renderWithTheme( + + ); + + await userEvent.click(getByLabelText('Firewall')); + + expect(queryByText(NO_FIREWALL_LABEL)).not.toBeInTheDocument(); + }); + + it('displays warning notice when "No firewall" is selected and warningMessageForNoFirewallOption is provided', () => { + const warningMessage = 'This Linode is not secured with a Cloud Firewall.'; + + const { getByText } = renderWithTheme( + + ); + + expect(getByText(warningMessage)).toBeVisible(); + }); + + it('does not display warning notice when "No firewall" is selected but warningMessageForNoFirewallOption is not provided', () => { + const { queryByRole } = renderWithTheme( + + ); + + expect(queryByRole('alert')).not.toBeInTheDocument(); + }); + + it('shows "No firewall" as selected when value is NO_FIREWALL_ID', async () => { + server.use( + http.get('*/v4/networking/firewalls', () => { + return HttpResponse.json(makeResourcePage([])); + }) + ); + + const { findByDisplayValue } = renderWithTheme( + + ); + + await findByDisplayValue(NO_FIREWALL_LABEL); + }); }); diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx index 0b17aaf6de8..3d4a819d683 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx @@ -1,5 +1,5 @@ import { useAllFirewallsQuery } from '@linode/queries'; -import { Autocomplete, InputAdornment } from '@linode/ui'; +import { Autocomplete, InputAdornment, Notice, Stack } from '@linode/ui'; import React, { useMemo } from 'react'; import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; @@ -10,6 +10,13 @@ import { FirewallSelectOption } from './FirewallSelectOption'; import type { Firewall } from '@linode/api-v4'; import type { EnhancedAutocompleteProps } from '@linode/ui'; +const NO_FIREWALL_ID = -1; + +const noFirewallOption = { + label: 'No firewall - traffic is unprotected (not recommended)', + id: NO_FIREWALL_ID, +} as Firewall; + interface Props extends Omit< EnhancedAutocompleteProps, @@ -31,10 +38,18 @@ interface Props * All Firewall will show if this is omitted. */ options?: Firewall[]; + /** + * Show an additional "No firewall (not recommended)" option in the dropdown, which has a value of `-1`. + */ + showNoFirewallOption?: boolean; /** * The ID of the selected Firewall */ value: null | number | undefined; + /** + * Warning notice when no firewall is selected. + */ + warningMessageForNoFirewallOption?: string; } /** @@ -47,50 +62,79 @@ interface Props export const FirewallSelect = ( props: Props ) => { - const { errorText, hideDefaultChips, label, loading, value, ...rest } = props; + const { + errorText, + hideDefaultChips, + label, + loading, + showNoFirewallOption = true, + value, + warningMessageForNoFirewallOption, + ...rest + } = props; const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); const { defaultNumEntities, isDefault, tooltipText } = useDefaultFirewallChipInformation(value, hideDefaultChips); + const options = useMemo( + () => [ + ...(firewalls ?? []), + ...(showNoFirewallOption ? [noFirewallOption] : []), + ], + [firewalls, showNoFirewallOption] + ); + const selectedFirewall = useMemo( - () => firewalls?.find((firewall) => firewall.id === value) ?? null, + () => + value === NO_FIREWALL_ID + ? noFirewallOption + : (firewalls?.find((firewall) => firewall.id === value) ?? null), [firewalls, value] ); return ( - - aria-label={label === '' ? 'Firewall' : undefined} - errorText={errorText ?? error?.[0].reason} - label={label ?? 'Firewall'} - loading={isLoading || loading} - noMarginTop - options={firewalls ?? []} - placeholder="None" - renderOption={({ key, ...props }, option, state) => ( - + + aria-label={label === '' ? 'Firewall' : undefined} + errorText={errorText ?? error?.[0].reason} + label={label ?? 'Firewall'} + loading={isLoading || loading} + noMarginTop + options={options} + placeholder="Select a Firewall" + renderOption={({ key, ...props }, option, state) => ( + + )} + textFieldProps={{ + InputProps: { + endAdornment: isDefault && !hideDefaultChips && ( + + + + ), + }, + }} + value={selectedFirewall!} + {...rest} + /> + {value === NO_FIREWALL_ID && warningMessageForNoFirewallOption && ( + )} - textFieldProps={{ - InputProps: { - endAdornment: isDefault && !hideDefaultChips && ( - - - - ), - }, - }} - value={selectedFirewall!} - {...rest} - /> + ); }; diff --git a/packages/manager/src/features/Kubernetes/NodePoolFirewallSelect.tsx b/packages/manager/src/features/Kubernetes/NodePoolFirewallSelect.tsx index a272cbebdc8..cc0008e5a50 100644 --- a/packages/manager/src/features/Kubernetes/NodePoolFirewallSelect.tsx +++ b/packages/manager/src/features/Kubernetes/NodePoolFirewallSelect.tsx @@ -137,6 +137,7 @@ export const NodePoolFirewallSelect = (props: NodePoolFirewallSelectProps) => { } }} placeholder="Select firewall" + showNoFirewallOption={false} value={field.value} /> )} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx index 93edc25385b..08c40cadcbc 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx @@ -14,6 +14,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; +import { WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION } from '../constants'; import { useGetLinodeCreateType } from './Tabs/utils/useGetLinodeCreateType'; import type { CreateLinodeRequest } from '@linode/api-v4'; @@ -108,8 +109,11 @@ export const Firewall = () => { }); } }} - placeholder="None" + placeholder="Select a Firewall" value={field.value} + warningMessageForNoFirewallOption={ + WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION + } /> { @@ -61,8 +63,11 @@ export const Firewall = () => { errorText={fieldState.error?.message} onBlur={field.onBlur} onChange={(e, firewall) => field.onChange(firewall?.id ?? null)} - placeholder="None" + placeholder="Select a Firewall" value={field.value} + warningMessageForNoFirewallOption={ + WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION + } /> { label={`${labelMap[interfaceType ?? 'public']} Interface Firewall`} onBlur={field.onBlur} onChange={(e, firewall) => field.onChange(firewall?.id ?? null)} - placeholder="None" + placeholder="Select a Firewall" value={field.value} + warningMessageForNoFirewallOption={ + WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION + } /> { field.onChange(value); // VLAN interfaces do not support Firewalls, so set - // the Firewall ID to `null` to be safe and early return. + // the Firewall ID to `-1` to be safe and early return. if (value === 'vlan') { - setValue(`linodeInterfaces.${index}.firewall_id`, null); + setValue(`linodeInterfaces.${index}.firewall_id`, -1); return; } diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx index 62642e6213c..01558429a41 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.tsx @@ -109,7 +109,7 @@ export const Summary = ({ isAlertsBetaMode }: SummaryProps) => { const hasFirewall = interfaceGeneration === 'linode' - ? linodeInterfaces.some((i) => i.firewall_id) + ? linodeInterfaces.some((i) => i.firewall_id && i.firewall_id !== -1) : firewallId; const hasBetaAclpAlertsAssigned = diff --git a/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts b/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts index dedae51d7de..dd798b23e45 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts @@ -38,6 +38,18 @@ export const getLinodeCreateResolver = ( values.linodeInterfaces = values.linodeInterfaces.map( getCleanedLinodeInterfaceValues ); + if ( + values.interface_generation === 'legacy_config' || + tab === 'Clone Linode' + ) { + // firewall_id is required in the form under interfaces object when using linode interfaces, but not when using legacy interfaces. + // If the user selects legacy interfaces, we set firewall_id to -1 to bypass the firewall requirement in the validation schema. + values.linodeInterfaces.forEach((linodeInterface) => { + linodeInterface.firewall_id = -1; + }); + } else { + values.firewall_id = -1; + } } else { values.linodeInterfaces = []; values.interfaces = @@ -52,6 +64,12 @@ export const getLinodeCreateResolver = ( values.metadata = undefined; } + // For the Clone Linode flow, we need not send firewall_id in the payload as API will take care of assigning the firewall_id based on the source Linode's configuration. + if (tab === 'Clone Linode' && !values.firewall_id) { + // The Clone Linode flow does not have the firewall_id field under interfaces object, so we set firewall_id to -1 to bypass the firewall requirement in the validation schema. + values.firewall_id = -1; + } + const { errors } = await yupResolver( schema, {}, diff --git a/packages/manager/src/features/Linodes/LinodeCreate/schemas.ts b/packages/manager/src/features/Linodes/LinodeCreate/schemas.ts index 2bc6cc15084..f51432af0fd 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/schemas.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/schemas.ts @@ -7,6 +7,7 @@ import { array, boolean, number, object, string } from 'yup'; import { CreateLinodeInterfaceFormSchema } from '../LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities'; import type { LinodeCreateFormValues } from './utilities'; +import type { InterfaceGenerationType } from '@linode/api-v4/lib/linodes/types'; import type { ObjectSchema } from 'yup'; /** @@ -17,6 +18,12 @@ import type { ObjectSchema } from 'yup'; export const CreateLinodeSchema: ObjectSchema = BaseCreateLinodeSchema.concat( object({ + firewall_id: number().when('interface_generation', { + is: (value: InterfaceGenerationType) => value === 'legacy_config', + then: (schema) => + schema.required('Select an option or create a new Firewall.'), + otherwise: (schema) => schema.nullable().notRequired(), + }), firewallOverride: boolean(), hasSignedEUAgreement: boolean(), interfaces: array(ConfigProfileInterfaceSchema).required(), diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx index 10ffa39e821..8dc0a441f17 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx @@ -489,6 +489,7 @@ describe('getDoesEmployeeNeedToAssignFirewall', () => { vpc: null, default_route: null, vlan: null, + firewall_id: null, }, ], 'linode' @@ -545,6 +546,7 @@ describe('getDoesEmployeeNeedToAssignFirewall', () => { public: null, default_route: null, vlan: { vlan_label: 'my-vlan-1' }, + firewall_id: null, }, ], 'linode' @@ -565,6 +567,7 @@ describe('getDoesEmployeeNeedToAssignFirewall', () => { vpc: null, vlan: null, default_route: null, + firewall_id: null, }, { vpc: null, @@ -572,6 +575,7 @@ describe('getDoesEmployeeNeedToAssignFirewall', () => { public: null, default_route: null, vlan: { vlan_label: 'my-vlan-1' }, + firewall_id: null, }, ], 'linode' diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx index 82b2370844e..d51e8701512 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx @@ -45,7 +45,7 @@ export const AddInterfaceForm = (props: Props) => { ) ?? []; const form = useForm({ defaultValues: { - firewall_id: null, + firewall_id: undefined, public: {}, vlan: {}, vpc: { @@ -62,7 +62,14 @@ export const AddInterfaceForm = (props: Props) => { const { errors, values } = await yupResolver( CreateLinodeInterfaceFormSchema - )(valuesWithOnlySelectedInterface, context, options); + )( + { + ...valuesWithOnlySelectedInterface, + firewall_id: valuesWithOnlySelectedInterface.firewall_id!, + }, + context, + options + ); if (errors) { return { errors, values }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx index e697993df09..a1e98f02870 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useController } from 'react-hook-form'; import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; +import { WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION } from 'src/features/Linodes/constants'; import type { CreateInterfaceFormValues } from './utilities'; @@ -18,8 +19,9 @@ export const InterfaceFirewall = () => { errorText={fieldState.error?.message} onBlur={field.onBlur} onChange={(e, firewall) => field.onChange(firewall?.id ?? null)} - placeholder="None" + placeholder="Select a Firewall" value={field.value} + warningMessageForNoFirewallOption={WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION} /> ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx index 83465ed7068..d09a81afcbc 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx @@ -39,9 +39,9 @@ export const InterfaceType = (props: Props) => { field.onChange(value); // VLAN interfaces do not support Firewalls, so set - // the Firewall ID to `null` to be safe and early return. + // the Firewall ID to `-1` to be safe and early return. if (value === 'vlan') { - setValue('firewall_id', null); + setValue('firewall_id', -1); return; } diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts index 9e3c41835f3..7f464ce0fba 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts @@ -9,6 +9,9 @@ import type { InferType } from 'yup'; export const CreateLinodeInterfaceFormSchema = CreateLinodeInterfaceSchema.concat( object({ + firewall_id: number().required( + 'Select an option or create a new Firewall.' + ), purpose: string() .oneOf(['vpc', 'vlan', 'public']) .required('You must selected an Interface type.'), diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/EditInterfaceDrawer/EditInterfaceFirewall.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/EditInterfaceDrawer/EditInterfaceFirewall.tsx index c83e38e881d..0a38715062d 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/EditInterfaceDrawer/EditInterfaceFirewall.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/EditInterfaceDrawer/EditInterfaceFirewall.tsx @@ -33,6 +33,7 @@ export const EditInterfaceFirewall = ({ showSuccessNotice }: Props) => { field.onChange(firewall?.id ?? null)} + showNoFirewallOption={false} value={field.value} /> )} diff --git a/packages/manager/src/features/Linodes/constants.ts b/packages/manager/src/features/Linodes/constants.ts index b1326b91364..63a2cd6c581 100644 --- a/packages/manager/src/features/Linodes/constants.ts +++ b/packages/manager/src/features/Linodes/constants.ts @@ -51,3 +51,6 @@ export const LINODE_LOCKED_DELETE_INTERFACE_TOOLTIP = export const LINODE_REBUILD_LOCKED_NOTICE_TEXT = 'This Linode is currently locked and cannot be rebuilt. Please remove the lock to proceed.'; + +export const WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION = + 'This Linode, or its Linode interface, is not secured with a Cloud Firewall. Add a firewall to help protect your resources and simplify security management.'; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 89d105ae4c8..6787031e6dc 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -29,6 +29,7 @@ import { Link } from 'src/components/Link'; import { RemovableSelectionsListTable } from 'src/components/RemovableSelectionsList/RemovableSelectionsListTable'; import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; import { useGetAllUserEntitiesByPermission } from 'src/features/IAM/hooks/useGetAllUserEntitiesByPermission'; +import { WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION } from 'src/features/Linodes/constants'; import { getDefaultFirewallForInterfacePurpose } from 'src/features/Linodes/LinodeCreate/Networking/utilities'; import { REMOVABLE_SELECTIONS_LINODES_TABLE_HEADERS, @@ -753,6 +754,9 @@ export const SubnetAssignLinodesDrawer = ( setFieldValue('selectedFirewall', firewall?.id) } value={values.selectedFirewall} + warningMessageForNoFirewallOption={ + WARNING_MESSAGE_FOR_NO_FIREWALL_OPTION + } /> )}