From f309f6da48f7e4cf57632d585af07365d8dafb33 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 8 Jul 2025 13:47:48 -0400 Subject: [PATCH 01/15] some types / comments --- .../mocks/presets/crud/handlers/linodes.ts | 70 +++++++++++++++++++ packages/manager/src/mocks/types.ts | 10 +-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts index a8ed0d083af..048a4ab39fe 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts @@ -29,6 +29,7 @@ import type { Disk, Firewall, FirewallDeviceEntityType, + Interface, InterfaceGenerationType, Linode, LinodeBackupsResponse, @@ -400,6 +401,7 @@ export const createLinode = (mockState: MockState) => [ ); } } else { + // TODO UPDATE THIS const linodeConfig = configFactory.build({ created: DateTime.now().toISO(), }); @@ -737,6 +739,8 @@ export const shutDownLinode = (mockState: MockState) => [ ), ]; +// Linode Interface handlers + export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ http.get( '*/v4*/linode/instances/:id/interfaces/:interfaceId/firewalls', @@ -1048,4 +1052,70 @@ export const updateLinodeInterfaceSettings = () => [ ), ]; +// Configuration Profile Interface handlers + +export const appendConfigInterface = (mockState: MockState) => [ + http.post( + '*/v4*/linode/instances/:id/configs/:configId/interfaces', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + const configId = Number(params.configId); + const config = await mswDB.get('linodeConfigs', configId); + + if (!linode || !config) { + return makeNotFoundResponse(); + } + + const payload = await request.clone().json(); + let linodeInterface; + + if (payload.vpc) { + linodeInterface = linodeInterfaceFactoryVPC.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } else if (payload.vlan) { + linodeInterface = linodeInterfaceFactoryVlan.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } else { + linodeInterface = linodeInterfaceFactoryPublic.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } + + await mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ); + + queueEvents({ + event: { + action: 'interface_create', + entity: { + id: linodeInterface.id, + label: linode.label, + type: 'linodeInterface', + url: `/v4beta/linodes/instances/${linode.id}/interfaces`, + }, + }, + mockState, + sequence: [{ status: 'finished' }], + }); + + return makeResponse(linodeInterface); + } + ), +]; + // TODO: ad more handlers (reboot, clone, resize, rebuild, rescue, migrate...) as needed diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index 5ef155c6b14..8f5da735f8b 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -6,6 +6,7 @@ import type { Event, Firewall, FirewallDevice, + Interface, IPAddress, KubeNodePoolResponse, KubernetesCluster, @@ -155,16 +156,17 @@ export type MockHandler = (mockState: MockState) => HttpHandler[]; */ export interface MockState { cloudnats: CloudNAT[]; + configInterfaces: [number, Interface][]; // number is Config ID domainRecords: DomainRecord[]; domains: Domain[]; eventQueue: Event[]; - firewallDevices: [number, FirewallDevice][]; + firewallDevices: [number, FirewallDevice][]; // number is Firewall ID firewalls: Firewall[]; ipAddresses: IPAddress[]; kubernetesClusters: KubernetesCluster[]; kubernetesNodePools: KubeNodePoolResponse[]; - linodeConfigs: [number, Config][]; - linodeInterfaces: [number, LinodeInterface][]; + linodeConfigs: [number, Config][]; // number is Linode ID + linodeInterfaces: [number, LinodeInterface][]; // number is Linode ID linodes: Linode[]; nodeBalancerConfigNodes: NodeBalancerConfigNode[]; nodeBalancerConfigs: NodeBalancerConfig[]; @@ -173,7 +175,7 @@ export interface MockState { placementGroups: PlacementGroup[]; regionAvailability: RegionAvailability[]; regions: Region[]; - subnets: [number, Subnet][]; + subnets: [number, Subnet][]; // number is VPC ID supportReplies: SupportReply[]; supportTickets: SupportTicket[]; volumes: Volume[]; From 03a3ec1a205ddd55875e7f51af113bf784708685 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 8 Jul 2025 14:00:09 -0400 Subject: [PATCH 02/15] connect back to VPC when creating Linode Interface --- .../mocks/presets/crud/handlers/linodes.ts | 113 +++++++++++------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts index 048a4ab39fe..65f945ed1c0 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts @@ -799,6 +799,65 @@ export const createLinodeInterface = (mockState: MockState) => [ created: DateTime.now().toISO(), updated: DateTime.now().toISO(), }); + + // Update corresponding VPC when creating a VPC Linode Interface + const subnetFromDB = await mswDB.get( + 'subnets', + payload.vpc.subnet_id ?? -1 + ); + const vpc = await mswDB.get( + 'vpcs', + payload.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 + ); + + if (subnetFromDB && vpc) { + const vpcInterface = linodeInterfaceFactoryVPC.build({ + ...payload, + default_route: { + ipv4: true, + }, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + + // update VPC/subnet to include this new interface + const updatedSubnet = { + ...subnetFromDB[1], + linodes: [ + ...subnetFromDB[1].linodes, + { + id: linode.id, + interfaces: [ + { + active: true, + config_id: null, + id: vpcInterface.id, + }, + ], + }, + ], + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } } else if (payload.vlan) { linodeInterface = linodeInterfaceFactoryVlan.build({ ...payload, @@ -819,6 +878,16 @@ export const createLinodeInterface = (mockState: MockState) => [ mockState ); + if (payload.firewall_id) { + await addFirewallDevice({ + entityId: linodeInterface.id, + entityLabel: linode.label, + firewallId: payload.firewall_id, + interfaceType: 'linode_interface', + mockState, + }); + } + queueEvents({ event: { action: 'interface_create', @@ -1071,49 +1140,7 @@ export const appendConfigInterface = (mockState: MockState) => [ } const payload = await request.clone().json(); - let linodeInterface; - - if (payload.vpc) { - linodeInterface = linodeInterfaceFactoryVPC.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } else if (payload.vlan) { - linodeInterface = linodeInterfaceFactoryVlan.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } else { - linodeInterface = linodeInterfaceFactoryPublic.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } - - await mswDB.add( - 'linodeInterfaces', - [linodeId, linodeInterface], - mockState - ); - - queueEvents({ - event: { - action: 'interface_create', - entity: { - id: linodeInterface.id, - label: linode.label, - type: 'linodeInterface', - url: `/v4beta/linodes/instances/${linode.id}/interfaces`, - }, - }, - mockState, - sequence: [{ status: 'finished' }], - }); - - return makeResponse(linodeInterface); + let configInterface; } ), ]; From 0607e7c2c0e4f703fb81584cbb125074abd94563 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 8 Jul 2025 18:01:59 -0400 Subject: [PATCH 03/15] rearrange files for organization --- .../presets/crud/handlers/linodes/configs.ts | 44 ++ .../crud/handlers/linodes/interfaces.ts | 480 +++++++++++++++++ .../crud/handlers/{ => linodes}/linodes.ts | 482 +----------------- .../manager/src/mocks/presets/crud/linodes.ts | 18 +- 4 files changed, 537 insertions(+), 487 deletions(-) create mode 100644 packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts create mode 100644 packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts rename packages/manager/src/mocks/presets/crud/handlers/{ => linodes}/linodes.ts (55%) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts new file mode 100644 index 00000000000..2afe67c2ce4 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -0,0 +1,44 @@ +import { DateTime } from 'luxon'; +import { http } from 'msw'; + +import { queueEvents } from 'src/mocks/utilities/events'; +import { + makeNotFoundResponse, + makePaginatedResponse, + makeResponse, +} from 'src/mocks/utilities/response'; + +import { mswDB } from '../../../../indexedDB'; +import { addFirewallDevice } from './linodes'; + +import type { + Config, +} from '@linode/api-v4'; +import type { StrictResponse } from 'msw'; +import type { MockState } from 'src/mocks/types'; +import type { + APIErrorResponse, + APIPaginatedResponse, +} from 'src/mocks/utilities/response'; + +export const appendConfigInterface = (mockState: MockState) => [ + http.post( + '*/v4*/linode/instances/:id/configs/:configId/interfaces', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + const configId = Number(params.configId); + const config = await mswDB.get('linodeConfigs', configId); + + if (!linode || !config) { + return makeNotFoundResponse(); + } + + const payload = await request.clone().json(); + let configInterface; + } + ), +]; \ No newline at end of file diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts new file mode 100644 index 00000000000..da27e9d3939 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -0,0 +1,480 @@ +import { + linodeInterfaceFactoryPublic, + linodeInterfaceFactoryVlan, + linodeInterfaceFactoryVPC, + linodeInterfaceSettingsFactory, +} from '@linode/utilities'; +import { DateTime } from 'luxon'; +import { http } from 'msw'; + +import { queueEvents } from 'src/mocks/utilities/events'; +import { + makeNotFoundResponse, + makePaginatedResponse, + makeResponse, +} from 'src/mocks/utilities/response'; + +import { mswDB } from '../../../../indexedDB'; +import { addFirewallDevice } from './linodes'; + +import type { + Config, + Firewall, + InterfaceGenerationType, + LinodeInterface, + LinodeInterfaces, + LinodeInterfaceSettings, + UpgradeInterfaceData, + UpgradeInterfacePayload, +} from '@linode/api-v4'; +import type { StrictResponse } from 'msw'; +import type { MockState } from 'src/mocks/types'; +import type { + APIErrorResponse, + APIPaginatedResponse, +} from 'src/mocks/utilities/response'; + +export const getInterfaces = () => [ + http.get( + '*/v4*/linode/instances/:id/interfaces', + async ({ + params, + }): Promise> => { + const id = Number(params.id); + const linode = await mswDB.get('linodes', id); + const linodeInterfaces = await mswDB.getAll('linodeInterfaces'); + + if ( + !linode || + !linodeInterfaces || + linode.interface_generation !== 'linode' + ) { + return makeNotFoundResponse(); + } + + const interfaces = linodeInterfaces + .filter((interfaceTuple) => interfaceTuple[0] === id) + .map((interfaceTuple) => interfaceTuple[1]); + + return makeResponse({ + interfaces, + }); + } + ), + + http.get( + '*/v4*/linode/instances/:id/interfaces/:interfaceId', + async ({ + params, + }): Promise> => { + const id = Number(params.id); + const interfaceId = Number(params.interfaceId); + const linode = await mswDB.get('linodes', id); + const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); + + if (!linode || !linodeInterface) { + return makeNotFoundResponse(); + } + + return makeResponse(linodeInterface[1]); + } + ), + + // todo: connect this to the DB eventually + http.get( + '*/v4*/linode/instances/:id/interfaces/settings', + async ({ + params, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + + if (!linode) { + return makeNotFoundResponse(); + } + + const linodeSettings = linodeInterfaceSettingsFactory.build(); + + return makeResponse(linodeSettings); + } + ), +] +export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ + http.get( + '*/v4*/linode/instances/:id/interfaces/:interfaceId/firewalls', + async ({ + params, + request, + }): Promise< + StrictResponse> + > => { + const linodeId = Number(params.id); + const interfaceId = Number(params.interfaceId); + const linode = mockState.linodes.find( + (stateLinode) => stateLinode.id === linodeId + ); + const linodeInterface = mockState.linodes.find( + (stateLinode) => stateLinode.id === interfaceId + ); + const allFirewalls = await mswDB.getAll('firewalls'); + + if (!linode || !linodeInterface || !allFirewalls) { + return makeNotFoundResponse(); + } + + const linodeInterfaceFirewalls = allFirewalls.filter((firewall) => + firewall.entities.some((entity) => entity.id === interfaceId) + ); + + return makePaginatedResponse({ + data: linodeInterfaceFirewalls, + request, + }); + } + ), +]; + +export const createLinodeInterface = (mockState: MockState) => [ + http.post( + '*/v4*/linode/instances/:id/interfaces', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + + if (!linode) { + return makeNotFoundResponse(); + } + + const payload = await request.clone().json(); + let linodeInterface; + + if (payload.vpc) { + linodeInterface = linodeInterfaceFactoryVPC.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + + // Update corresponding VPC when creating a VPC Linode Interface + const subnetFromDB = await mswDB.get( + 'subnets', + payload.vpc.subnet_id ?? -1 + ); + const vpc = await mswDB.get( + 'vpcs', + payload.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 + ); + + if (subnetFromDB && vpc) { + const vpcInterface = linodeInterfaceFactoryVPC.build({ + ...payload, + default_route: { + ipv4: true, + }, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + + // update VPC/subnet to include this new interface + const updatedSubnet = { + ...subnetFromDB[1], + linodes: [ + ...subnetFromDB[1].linodes, + { + id: linode.id, + interfaces: [ + { + active: true, + config_id: null, + id: vpcInterface.id, + }, + ], + }, + ], + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } + } else if (payload.vlan) { + linodeInterface = linodeInterfaceFactoryVlan.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } else { + linodeInterface = linodeInterfaceFactoryPublic.build({ + ...payload, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } + + await mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ); + + if (payload.firewall_id) { + await addFirewallDevice({ + entityId: linodeInterface.id, + entityLabel: linode.label, + firewallId: payload.firewall_id, + interfaceType: 'linode_interface', + mockState, + }); + } + + queueEvents({ + event: { + action: 'interface_create', + entity: { + id: linodeInterface.id, + label: linode.label, + type: 'linodeInterface', + url: `/v4beta/linodes/instances/${linode.id}/interfaces`, + }, + }, + mockState, + sequence: [{ status: 'finished' }], + }); + + return makeResponse(linodeInterface); + } + ), +]; + +export const deleteLinodeInterface = (mockState: MockState) => [ + http.delete( + '*/v4*/linodes/instances/:id/interfaces/:interfaceId', + async ({ params }): Promise> => { + const linodeId = Number(params.id); + const interfaceId = Number(params.interfaceId); + const linode = await mswDB.get('linodes', linodeId); + const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); + + if (!linode || !linodeInterface) { + return makeNotFoundResponse(); + } + + await mswDB.delete('linodeInterfaces', interfaceId, mockState); + + queueEvents({ + event: { + action: 'interface_delete', + entity: { + id: interfaceId, + label: linode.label, + type: 'interface', + url: `/v4beta/linodes/instances/${linode.id}/interfaces`, + }, + }, + mockState, + sequence: [{ status: 'finished' }], + }); + + return makeResponse({}); + } + ), +]; + +export const updateLinodeInterface = (mockState: MockState) => [ + http.put( + '*/v4*/linodes/instances/:id/interfaces/:interfaceId', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const interfaceId = Number(params.interfaceId); + const linode = await mswDB.get('linodes', linodeId); + const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); + + if (!linode || !linodeInterface) { + return makeNotFoundResponse(); + } + + const payload = await request.clone().json(); + + const updatedInterface = { + ...linodeInterface[1], + ...payload, + updated: DateTime.now().toISO(), + }; + + await mswDB.update( + 'linodeInterfaces', + interfaceId, + [linodeId, updatedInterface], + mockState + ); + + queueEvents({ + event: { + action: 'interface_update', + entity: { + id: interfaceId, + label: linode.label, + type: 'subnets', + url: `/v4beta/linodes/instances/${linode.id}/interfaces`, + }, + }, + mockState, + sequence: [{ status: 'notification' }], + }); + + return makeResponse(updatedInterface); + } + ), +]; + +const convertToLinodeInterfaces = (config: Config | undefined) => { + const linodeInterfacePublic = linodeInterfaceFactoryPublic.build({ + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + if (!config || config.interfaces?.length === 0) { + return [linodeInterfacePublic]; + } + return ( + config.interfaces?.map((iface) => { + if (iface.purpose === 'public') { + return linodeInterfacePublic; + } else if (iface.purpose === 'vlan') { + return linodeInterfaceFactoryVlan.build({ + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } else { + return linodeInterfaceFactoryVPC.build({ + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } + }) ?? [linodeInterfacePublic] + ); +}; + +export const upgradeToLinodeInterfaces = (mockState: MockState) => [ + http.post( + '*/v4*/linode/instances/:id/upgrade-interfaces', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + const linodeConfigs = await mswDB.getAll('linodeConfigs'); + + if (!linode || !linodeConfigs) { + return makeNotFoundResponse(); + } + + const configs = linodeConfigs + .filter((configTuple) => configTuple[0] === linodeId) + .map((configTuple) => configTuple[1]); + + const payload: UpgradeInterfacePayload = { + ...(await request.clone().json()), + }; + + const { config_id, dry_run } = payload; + const config = + configs.find((config) => config.id === config_id) ?? configs[0]; + + const linodeInterfaces = convertToLinodeInterfaces(config); + + const addLinodeInterfacePromises = []; + const updateConfigPromises = []; + + // if not a dry run, update everything + if (dry_run === false) { + // for all configs, remove the interfaces + const updatedConfigs = configs.map((config) => { + return { ...config, interfaces: null }; + }); + const updatedLinode = { + ...linode, + interface_generation: 'linode' as InterfaceGenerationType, + }; + + for (const linodeInterface of linodeInterfaces) { + addLinodeInterfacePromises.push( + mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ) + ); + } + + for (const updatedConfig of updatedConfigs) { + updateConfigPromises.push( + mswDB.update( + 'linodeConfigs', + config.id, + [linodeId, updatedConfig], + mockState + ) + ); + } + + await Promise.all(addLinodeInterfacePromises); + await Promise.all(updateConfigPromises); + await mswDB.update('linodes', linodeId, updatedLinode, mockState); + } + + return makeResponse({ + config_id: config_id ?? config.id ?? -1, + dry_run: dry_run ?? true, + interfaces: linodeInterfaces, + }); + } + ), +]; + +export const updateLinodeInterfaceSettings = () => [ + http.put( + '*/v4*/linodes/instances/:id/interfaces/settings', + async ({ + params, + request, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + + if (!linode) { + return makeNotFoundResponse(); + } + + const payload = await request.clone().json(); + + const updatedSettings = linodeInterfaceSettingsFactory.build({ + ...payload, + }); + + return makeResponse(updatedSettings); + } + ), +]; diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts similarity index 55% rename from packages/manager/src/mocks/presets/crud/handlers/linodes.ts rename to packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts index 65f945ed1c0..81f29244100 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts @@ -5,7 +5,6 @@ import { linodeInterfaceFactoryPublic, linodeInterfaceFactoryVlan, linodeInterfaceFactoryVPC, - linodeInterfaceSettingsFactory, linodeIPFactory, linodeStatsFactory, linodeTransferFactory, @@ -21,7 +20,7 @@ import { makeResponse, } from 'src/mocks/utilities/response'; -import { mswDB } from '../../../indexedDB'; +import { mswDB } from '../../../../indexedDB'; import type { Config, @@ -29,18 +28,11 @@ import type { Disk, Firewall, FirewallDeviceEntityType, - Interface, - InterfaceGenerationType, Linode, LinodeBackupsResponse, - LinodeInterface, - LinodeInterfaces, - LinodeInterfaceSettings, LinodeIPsResponse, RegionalNetworkUtilization, Stats, - UpgradeInterfaceData, - UpgradeInterfacePayload, } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; import type { MockState } from 'src/mocks/types'; @@ -84,70 +76,6 @@ export const getLinodes = () => [ } ), - http.get( - '*/v4*/linode/instances/:id/interfaces', - async ({ - params, - }): Promise> => { - const id = Number(params.id); - const linode = await mswDB.get('linodes', id); - const linodeInterfaces = await mswDB.getAll('linodeInterfaces'); - - if ( - !linode || - !linodeInterfaces || - linode.interface_generation !== 'linode' - ) { - return makeNotFoundResponse(); - } - - const interfaces = linodeInterfaces - .filter((interfaceTuple) => interfaceTuple[0] === id) - .map((interfaceTuple) => interfaceTuple[1]); - - return makeResponse({ - interfaces, - }); - } - ), - - http.get( - '*/v4*/linode/instances/:id/interfaces/:interfaceId', - async ({ - params, - }): Promise> => { - const id = Number(params.id); - const interfaceId = Number(params.interfaceId); - const linode = await mswDB.get('linodes', id); - const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); - - if (!linode || !linodeInterface) { - return makeNotFoundResponse(); - } - - return makeResponse(linodeInterface[1]); - } - ), - - // todo: connect this to the DB eventually - http.get( - '*/v4*/linode/instances/:id/interfaces/settings', - async ({ - params, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - - if (!linode) { - return makeNotFoundResponse(); - } - - const linodeSettings = linodeInterfaceSettingsFactory.build(); - - return makeResponse(linodeSettings); - } - ), - http.get( '*/v4/linode/instances/:id/configs', async ({ @@ -176,7 +104,7 @@ export const getLinodes = () => [ ), ]; -const addFirewallDevice = async (inputs: { +export const addFirewallDevice = async (inputs: { entityId: number; entityLabel: string; firewallId: number; @@ -739,410 +667,4 @@ export const shutDownLinode = (mockState: MockState) => [ ), ]; -// Linode Interface handlers - -export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ - http.get( - '*/v4*/linode/instances/:id/interfaces/:interfaceId/firewalls', - async ({ - params, - request, - }): Promise< - StrictResponse> - > => { - const linodeId = Number(params.id); - const interfaceId = Number(params.interfaceId); - const linode = mockState.linodes.find( - (stateLinode) => stateLinode.id === linodeId - ); - const linodeInterface = mockState.linodes.find( - (stateLinode) => stateLinode.id === interfaceId - ); - const allFirewalls = await mswDB.getAll('firewalls'); - - if (!linode || !linodeInterface || !allFirewalls) { - return makeNotFoundResponse(); - } - - const linodeInterfaceFirewalls = allFirewalls.filter((firewall) => - firewall.entities.some((entity) => entity.id === interfaceId) - ); - - return makePaginatedResponse({ - data: linodeInterfaceFirewalls, - request, - }); - } - ), -]; - -export const createLinodeInterface = (mockState: MockState) => [ - http.post( - '*/v4*/linode/instances/:id/interfaces', - async ({ - params, - request, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - - if (!linode) { - return makeNotFoundResponse(); - } - - const payload = await request.clone().json(); - let linodeInterface; - - if (payload.vpc) { - linodeInterface = linodeInterfaceFactoryVPC.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - - // Update corresponding VPC when creating a VPC Linode Interface - const subnetFromDB = await mswDB.get( - 'subnets', - payload.vpc.subnet_id ?? -1 - ); - const vpc = await mswDB.get( - 'vpcs', - payload.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 - ); - - if (subnetFromDB && vpc) { - const vpcInterface = linodeInterfaceFactoryVPC.build({ - ...payload, - default_route: { - ipv4: true, - }, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - - // update VPC/subnet to include this new interface - const updatedSubnet = { - ...subnetFromDB[1], - linodes: [ - ...subnetFromDB[1].linodes, - { - id: linode.id, - interfaces: [ - { - active: true, - config_id: null, - id: vpcInterface.id, - }, - ], - }, - ], - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - } - } else if (payload.vlan) { - linodeInterface = linodeInterfaceFactoryVlan.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } else { - linodeInterface = linodeInterfaceFactoryPublic.build({ - ...payload, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } - - await mswDB.add( - 'linodeInterfaces', - [linodeId, linodeInterface], - mockState - ); - - if (payload.firewall_id) { - await addFirewallDevice({ - entityId: linodeInterface.id, - entityLabel: linode.label, - firewallId: payload.firewall_id, - interfaceType: 'linode_interface', - mockState, - }); - } - - queueEvents({ - event: { - action: 'interface_create', - entity: { - id: linodeInterface.id, - label: linode.label, - type: 'linodeInterface', - url: `/v4beta/linodes/instances/${linode.id}/interfaces`, - }, - }, - mockState, - sequence: [{ status: 'finished' }], - }); - - return makeResponse(linodeInterface); - } - ), -]; - -export const deleteLinodeInterface = (mockState: MockState) => [ - http.delete( - '*/v4*/linodes/instances/:id/interfaces/:interfaceId', - async ({ params }): Promise> => { - const linodeId = Number(params.id); - const interfaceId = Number(params.interfaceId); - const linode = await mswDB.get('linodes', linodeId); - const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); - - if (!linode || !linodeInterface) { - return makeNotFoundResponse(); - } - - await mswDB.delete('linodeInterfaces', interfaceId, mockState); - - queueEvents({ - event: { - action: 'interface_delete', - entity: { - id: interfaceId, - label: linode.label, - type: 'interface', - url: `/v4beta/linodes/instances/${linode.id}/interfaces`, - }, - }, - mockState, - sequence: [{ status: 'finished' }], - }); - - return makeResponse({}); - } - ), -]; - -export const updateLinodeInterface = (mockState: MockState) => [ - http.put( - '*/v4*/linodes/instances/:id/interfaces/:interfaceId', - async ({ - params, - request, - }): Promise> => { - const linodeId = Number(params.id); - const interfaceId = Number(params.interfaceId); - const linode = await mswDB.get('linodes', linodeId); - const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); - - if (!linode || !linodeInterface) { - return makeNotFoundResponse(); - } - - const payload = await request.clone().json(); - - const updatedInterface = { - ...linodeInterface[1], - ...payload, - updated: DateTime.now().toISO(), - }; - - await mswDB.update( - 'linodeInterfaces', - interfaceId, - [linodeId, updatedInterface], - mockState - ); - - queueEvents({ - event: { - action: 'interface_update', - entity: { - id: interfaceId, - label: linode.label, - type: 'subnets', - url: `/v4beta/linodes/instances/${linode.id}/interfaces`, - }, - }, - mockState, - sequence: [{ status: 'notification' }], - }); - - return makeResponse(updatedInterface); - } - ), -]; - -const convertToLinodeInterfaces = (config: Config | undefined) => { - const linodeInterfacePublic = linodeInterfaceFactoryPublic.build({ - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - if (!config || config.interfaces?.length === 0) { - return [linodeInterfacePublic]; - } - return ( - config.interfaces?.map((iface) => { - if (iface.purpose === 'public') { - return linodeInterfacePublic; - } else if (iface.purpose === 'vlan') { - return linodeInterfaceFactoryVlan.build({ - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } else { - return linodeInterfaceFactoryVPC.build({ - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); - } - }) ?? [linodeInterfacePublic] - ); -}; - -export const upgradeToLinodeInterfaces = (mockState: MockState) => [ - http.post( - '*/v4*/linode/instances/:id/upgrade-interfaces', - async ({ - params, - request, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - const linodeConfigs = await mswDB.getAll('linodeConfigs'); - - if (!linode || !linodeConfigs) { - return makeNotFoundResponse(); - } - - const configs = linodeConfigs - .filter((configTuple) => configTuple[0] === linodeId) - .map((configTuple) => configTuple[1]); - - const payload: UpgradeInterfacePayload = { - ...(await request.clone().json()), - }; - - const { config_id, dry_run } = payload; - const config = - configs.find((config) => config.id === config_id) ?? configs[0]; - - const linodeInterfaces = convertToLinodeInterfaces(config); - - const addLinodeInterfacePromises = []; - const updateConfigPromises = []; - - // if not a dry run, update everything - if (dry_run === false) { - // for all configs, remove the interfaces - const updatedConfigs = configs.map((config) => { - return { ...config, interfaces: null }; - }); - const updatedLinode = { - ...linode, - interface_generation: 'linode' as InterfaceGenerationType, - }; - - for (const linodeInterface of linodeInterfaces) { - addLinodeInterfacePromises.push( - mswDB.add( - 'linodeInterfaces', - [linodeId, linodeInterface], - mockState - ) - ); - } - - for (const updatedConfig of updatedConfigs) { - updateConfigPromises.push( - mswDB.update( - 'linodeConfigs', - config.id, - [linodeId, updatedConfig], - mockState - ) - ); - } - - await Promise.all(addLinodeInterfacePromises); - await Promise.all(updateConfigPromises); - await mswDB.update('linodes', linodeId, updatedLinode, mockState); - } - - return makeResponse({ - config_id: config_id ?? config.id ?? -1, - dry_run: dry_run ?? true, - interfaces: linodeInterfaces, - }); - } - ), -]; - -export const updateLinodeInterfaceSettings = () => [ - http.put( - '*/v4*/linodes/instances/:id/interfaces/settings', - async ({ - params, - request, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - - if (!linode) { - return makeNotFoundResponse(); - } - - const payload = await request.clone().json(); - - const updatedSettings = linodeInterfaceSettingsFactory.build({ - ...payload, - }); - - return makeResponse(updatedSettings); - } - ), -]; - -// Configuration Profile Interface handlers - -export const appendConfigInterface = (mockState: MockState) => [ - http.post( - '*/v4*/linode/instances/:id/configs/:configId/interfaces', - async ({ - params, - request, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - const configId = Number(params.configId); - const config = await mswDB.get('linodeConfigs', configId); - - if (!linode || !config) { - return makeNotFoundResponse(); - } - - const payload = await request.clone().json(); - let configInterface; - } - ), -]; - // TODO: ad more handlers (reboot, clone, resize, rebuild, rescue, migrate...) as needed diff --git a/packages/manager/src/mocks/presets/crud/linodes.ts b/packages/manager/src/mocks/presets/crud/linodes.ts index 4a4a2247c9f..661ce0814fa 100644 --- a/packages/manager/src/mocks/presets/crud/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/linodes.ts @@ -1,22 +1,25 @@ import { - createLinode, createLinodeInterface, - deleteLinode, deleteLinodeInterface, + getInterfaces, + getLinodeInterfaceFirewalls, + updateLinodeInterface, + updateLinodeInterfaceSettings, + upgradeToLinodeInterfaces, +} from 'src/mocks/presets/crud/handlers/linodes/interfaces'; +import { + createLinode, + deleteLinode, getLinodeBackups, getLinodeDisks, getLinodeFirewalls, - getLinodeInterfaceFirewalls, getLinodeIps, getLinodes, getLinodeStats, getLinodeTransfer, shutDownLinode, updateLinode, - updateLinodeInterface, - updateLinodeInterfaceSettings, - upgradeToLinodeInterfaces, -} from 'src/mocks/presets/crud/handlers/linodes'; +} from 'src/mocks/presets/crud/handlers/linodes/linodes'; import type { MockPresetCrud } from 'src/mocks/types'; @@ -27,6 +30,7 @@ export const linodeCrudPreset: MockPresetCrud = { createLinode, updateLinode, deleteLinode, + getInterfaces, getLinodeStats, getLinodeDisks, getLinodeFirewalls, From d35b728bc6e9fe005634ee861b5361f31b308604 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 9 Jul 2025 08:56:00 -0400 Subject: [PATCH 04/15] don't save configInterfaces actually - more complicated --- packages/manager/src/mocks/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index 8f5da735f8b..6b161121266 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -156,7 +156,6 @@ export type MockHandler = (mockState: MockState) => HttpHandler[]; */ export interface MockState { cloudnats: CloudNAT[]; - configInterfaces: [number, Interface][]; // number is Config ID domainRecords: DomainRecord[]; domains: Domain[]; eventQueue: Event[]; From bc842c472d21fd96170ce00e761e38da90b4bd3b Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 9 Jul 2025 09:04:34 -0400 Subject: [PATCH 05/15] fix some id issues --- .../crud/handlers/linodes/interfaces.ts | 29 ++++++++++--------- .../presets/crud/handlers/linodes/linodes.ts | 14 +++++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index da27e9d3939..7553def8dd2 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -158,6 +158,12 @@ export const createLinodeInterface = (mockState: MockState) => [ updated: DateTime.now().toISO(), }); + const createdInterface = await mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ); + // Update corresponding VPC when creating a VPC Linode Interface const subnetFromDB = await mswDB.get( 'subnets', @@ -169,14 +175,6 @@ export const createLinodeInterface = (mockState: MockState) => [ ); if (subnetFromDB && vpc) { - const vpcInterface = linodeInterfaceFactoryVPC.build({ - ...payload, - default_route: { - ipv4: true, - }, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); // update VPC/subnet to include this new interface const updatedSubnet = { @@ -189,7 +187,8 @@ export const createLinodeInterface = (mockState: MockState) => [ { active: true, config_id: null, - id: vpcInterface.id, + // ensure interface ID in subnet matches interface ID in DB + id: createdInterface[1].id, }, ], }, @@ -230,11 +229,13 @@ export const createLinodeInterface = (mockState: MockState) => [ }); } - await mswDB.add( - 'linodeInterfaces', - [linodeId, linodeInterface], - mockState - ); + if (!payload.vpc) { + await mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ); + } if (payload.firewall_id) { await addFirewallDevice({ diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts index 81f29244100..92d5b89cb98 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts @@ -225,6 +225,12 @@ export const createLinode = (mockState: MockState) => [ updated: DateTime.now().toISO(), }); + const createdVPCInterface = await mswDB.add( + 'linodeInterfaces', + [linode.id, vpcInterface], + mockState + ); + // update VPC/subnet to include this new interface const updatedSubnet = { ...subnetFromDB[1], @@ -236,7 +242,8 @@ export const createLinode = (mockState: MockState) => [ { active: true, config_id: null, - id: vpcInterface.id, + // ensure interface ID in subnet matches interface ID in DB + id: createdVPCInterface[1].id, }, ], }, @@ -255,11 +262,6 @@ export const createLinode = (mockState: MockState) => [ }), }; - await mswDB.add( - 'linodeInterfaces', - [linode.id, vpcInterface], - mockState - ); await mswDB.update( 'subnets', subnetFromDB[1].id, From a58e790a638205a07232c5dc278362e0ca912117 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 9 Jul 2025 22:53:48 -0400 Subject: [PATCH 06/15] append config interface --- .../presets/crud/handlers/linodes/configs.ts | 80 +++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index 2afe67c2ce4..49cb049a3ce 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -1,3 +1,4 @@ +import { linodeConfigInterfaceFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import { http } from 'msw'; @@ -11,9 +12,7 @@ import { import { mswDB } from '../../../../indexedDB'; import { addFirewallDevice } from './linodes'; -import type { - Config, -} from '@linode/api-v4'; +import type { Config, Interface } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; import type { MockState } from 'src/mocks/types'; import type { @@ -37,8 +36,77 @@ export const appendConfigInterface = (mockState: MockState) => [ return makeNotFoundResponse(); } - const payload = await request.clone().json(); - let configInterface; + const interfacePayload = await request.clone().json(); + const configInterface = linodeConfigInterfaceFactory.build({ + ...interfacePayload, + active: true, + }); + const updatedConfig = { + ...config[1], + interfaces: [...(config[1].interfaces ?? []), configInterface], + }; + + // @TODO CONNIE - move to helper function of sorts + if (interfacePayload.purpose === 'vpc') { + // Update corresponding VPC when creating a VPC interface + const subnetFromDB = await mswDB.get( + 'subnets', + configInterface.subnet_id ?? -1 + ); + const vpc = await mswDB.get( + 'vpcs', + configInterface.vpc_id ?? subnetFromDB?.[0] ?? -1 + ); + + if (subnetFromDB && vpc) { + // update VPC/subnet to include this new interface + const updatedSubnet = { + ...subnetFromDB[1], + linodes: [ + ...subnetFromDB[1].linodes, + { + id: linode.id, + interfaces: [ + { + active: true, + config_id: configId, + id: configInterface.id, + }, + ], + }, + ], + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } + } + + await mswDB.update( + 'linodeConfigs', + configId, + [linode.id, updatedConfig], + mockState + ); + + return makeResponse(configInterface); } ), -]; \ No newline at end of file +]; From 140c5e038511fa009b4704887fc796bf538899be Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 9 Jul 2025 23:13:50 -0400 Subject: [PATCH 07/15] i'm thinking...maybe --- .../presets/crud/handlers/linodes/configs.ts | 42 +++++++++++++++++++ .../crud/handlers/linodes/interfaces.ts | 2 + 2 files changed, 44 insertions(+) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index 49cb049a3ce..287a1feb85e 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -70,6 +70,11 @@ export const appendConfigInterface = (mockState: MockState) => [ { active: true, config_id: configId, + // NOTE: configInterface.id may be duplicated across all subnets, as we are not storing + // a separate configInterface field in our mock DB. This is to reduce complexity (ie needing to fetch/update + // a config and then an interface for our mock operations) + // We currently do not fetch/update individual config interfaces anyway + // See VPC/Subnet delete crud handlers id: configInterface.id, }, ], @@ -110,3 +115,40 @@ export const appendConfigInterface = (mockState: MockState) => [ } ), ]; + +// deleteLinodeConfigInterface +export const deleteLinodeConfigInterface = (mockState: MockState) => [ + http.delete( + '*/v4*/linodes/instances/:id/configs/:configId/interfaces/:interfaceId', + async ({ params }): Promise> => { + const linodeId = Number(params.id); + const configId = Number(params.configId); + const linode = await mswDB.get('linodes', linodeId); + const config = await mswDB.get('linodeConfigs', configId); + + if (!linode || !config) { + return makeNotFoundResponse(); + } + + const updatedConfig = { + ...config[1], + interfaces: [], + }; + + // to "delete" the interface, we will set the config's interfaces to []. While this isn't accurate to the actual API, this reduces complexity on our end + // update this may be changed ... i'm thinking some more + await mswDB.update( + 'linodeConfigs', + configId, + [linode.id, updatedConfig], + mockState + ); + + return makeResponse({}); + } + ), +]; + +// todo: potentially add back in storing the interfaces separately - need to think about how complicated vpc / subnet deletion can be :sobbing: +// OH ANOTHER THOUGHT - what if we always leave configs.interfaces empty, but when we actually return a config, we must also make a request to its interfaces? +// maybe that makes more sense... (don't need to update the config in tandem -> help out with deleting subnet / vpc) \ No newline at end of file diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index 7553def8dd2..0c9c40aeac7 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -281,6 +281,8 @@ export const deleteLinodeInterface = (mockState: MockState) => [ await mswDB.delete('linodeInterfaces', interfaceId, mockState); + // todo connie: add vpc stuff... + queueEvents({ event: { action: 'interface_delete', From bd64a8c7878db4b30369444d32168a94c336e2ae Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 10 Jul 2025 12:08:40 -0400 Subject: [PATCH 08/15] update subnet/vpc updating logic --- packages/manager/src/mocks/mockState.ts | 1 + .../presets/crud/handlers/linodes/configs.ts | 101 +++++++++++------- packages/manager/src/mocks/types.ts | 1 + 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/packages/manager/src/mocks/mockState.ts b/packages/manager/src/mocks/mockState.ts index 915df936c1c..6e1b259cec7 100644 --- a/packages/manager/src/mocks/mockState.ts +++ b/packages/manager/src/mocks/mockState.ts @@ -23,6 +23,7 @@ export const getStateSeederGroups = ( export const emptyStore: MockState = { cloudnats: [], + configInterfaces: [], domainRecords: [], domains: [], eventQueue: [], diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index 287a1feb85e..96fa0c4a58f 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -41,10 +41,12 @@ export const appendConfigInterface = (mockState: MockState) => [ ...interfacePayload, active: true, }); - const updatedConfig = { - ...config[1], - interfaces: [...(config[1].interfaces ?? []), configInterface], - }; + + const newlyAddedConfigInterface = await mswDB.add( + 'configInterfaces', + [linode.id, configInterface], + mockState + ); // @TODO CONNIE - move to helper function of sorts if (interfacePayload.purpose === 'vpc') { @@ -70,12 +72,7 @@ export const appendConfigInterface = (mockState: MockState) => [ { active: true, config_id: configId, - // NOTE: configInterface.id may be duplicated across all subnets, as we are not storing - // a separate configInterface field in our mock DB. This is to reduce complexity (ie needing to fetch/update - // a config and then an interface for our mock operations) - // We currently do not fetch/update individual config interfaces anyway - // See VPC/Subnet delete crud handlers - id: configInterface.id, + id: newlyAddedConfigInterface[1].id, }, ], }, @@ -104,14 +101,7 @@ export const appendConfigInterface = (mockState: MockState) => [ } } - await mswDB.update( - 'linodeConfigs', - configId, - [linode.id, updatedConfig], - mockState - ); - - return makeResponse(configInterface); + return makeResponse(newlyAddedConfigInterface[1]); } ), ]; @@ -123,32 +113,71 @@ export const deleteLinodeConfigInterface = (mockState: MockState) => [ async ({ params }): Promise> => { const linodeId = Number(params.id); const configId = Number(params.configId); + const interfaceId = Number(params.interfaceId); const linode = await mswDB.get('linodes', linodeId); const config = await mswDB.get('linodeConfigs', configId); + const configInterfaceTuple = await mswDB.get( + 'configInterfaces', + interfaceId + ); - if (!linode || !config) { + if (!linode || !config || !configInterfaceTuple) { return makeNotFoundResponse(); } - const updatedConfig = { - ...config[1], - interfaces: [], - }; - - // to "delete" the interface, we will set the config's interfaces to []. While this isn't accurate to the actual API, this reduces complexity on our end - // update this may be changed ... i'm thinking some more - await mswDB.update( - 'linodeConfigs', - configId, - [linode.id, updatedConfig], - mockState - ); + // if the config interface is part of a VPC, we must update the VPC as well + const configInterface = configInterfaceTuple[1]; + if (configInterface.purpose === 'vpc' && configInterface.subnet_id) { + const subnetFromDB = await mswDB.get( + 'subnets', + configInterface.subnet_id + ); + const vpc = await mswDB.get( + 'vpcs', + configInterface.vpc_id ?? subnetFromDB?.[0] ?? -1 + ); + + if (subnetFromDB && vpc) { + // update VPC/subnet to remove interface + const updatedLinodeData = subnetFromDB[1].linodes.map((data) => { + return { + ...data, + interfaces: data.interfaces.filter( + (iface) => iface.id !== interfaceId + ), + }; + }); + + const updatedSubnet = { + ...subnetFromDB[1], + linodes: updatedLinodeData, + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } + } + + await mswDB.delete('configInterfaces', interfaceId, mockState); return makeResponse({}); } ), ]; - -// todo: potentially add back in storing the interfaces separately - need to think about how complicated vpc / subnet deletion can be :sobbing: -// OH ANOTHER THOUGHT - what if we always leave configs.interfaces empty, but when we actually return a config, we must also make a request to its interfaces? -// maybe that makes more sense... (don't need to update the config in tandem -> help out with deleting subnet / vpc) \ No newline at end of file diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index 6b161121266..8f5da735f8b 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -156,6 +156,7 @@ export type MockHandler = (mockState: MockState) => HttpHandler[]; */ export interface MockState { cloudnats: CloudNAT[]; + configInterfaces: [number, Interface][]; // number is Config ID domainRecords: DomainRecord[]; domains: Domain[]; eventQueue: Event[]; From e1e3d15ab8d82524bc5d5273a2a8a37a0bc0c115 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 10 Jul 2025 13:53:43 -0400 Subject: [PATCH 09/15] update interface deletion --- .../presets/crud/handlers/linodes/configs.ts | 17 ++--- .../crud/handlers/linodes/interfaces.ts | 64 +++++++++++++++++-- .../manager/src/mocks/presets/crud/linodes.ts | 7 ++ 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index 96fa0c4a58f..da43d459c8c 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -2,23 +2,17 @@ import { linodeConfigInterfaceFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import { http } from 'msw'; -import { queueEvents } from 'src/mocks/utilities/events'; import { makeNotFoundResponse, - makePaginatedResponse, makeResponse, } from 'src/mocks/utilities/response'; import { mswDB } from '../../../../indexedDB'; -import { addFirewallDevice } from './linodes'; -import type { Config, Interface } from '@linode/api-v4'; +import type { Interface } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; import type { MockState } from 'src/mocks/types'; -import type { - APIErrorResponse, - APIPaginatedResponse, -} from 'src/mocks/utilities/response'; +import type { APIErrorResponse } from 'src/mocks/utilities/response'; export const appendConfigInterface = (mockState: MockState) => [ http.post( @@ -48,7 +42,6 @@ export const appendConfigInterface = (mockState: MockState) => [ mockState ); - // @TODO CONNIE - move to helper function of sorts if (interfacePayload.purpose === 'vpc') { // Update corresponding VPC when creating a VPC interface const subnetFromDB = await mswDB.get( @@ -106,8 +99,7 @@ export const appendConfigInterface = (mockState: MockState) => [ ), ]; -// deleteLinodeConfigInterface -export const deleteLinodeConfigInterface = (mockState: MockState) => [ +export const deleteConfigInterface = (mockState: MockState) => [ http.delete( '*/v4*/linodes/instances/:id/configs/:configId/interfaces/:interfaceId', async ({ params }): Promise> => { @@ -143,7 +135,8 @@ export const deleteLinodeConfigInterface = (mockState: MockState) => [ return { ...data, interfaces: data.interfaces.filter( - (iface) => iface.id !== interfaceId + (iface) => + iface.id !== interfaceId && iface.config_id === configId ), }; }); diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index 0c9c40aeac7..4987af5142c 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -98,7 +98,7 @@ export const getInterfaces = () => [ return makeResponse(linodeSettings); } ), -] +]; export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ http.get( '*/v4*/linode/instances/:id/interfaces/:interfaceId/firewalls', @@ -175,7 +175,6 @@ export const createLinodeInterface = (mockState: MockState) => [ ); if (subnetFromDB && vpc) { - // update VPC/subnet to include this new interface const updatedSubnet = { ...subnetFromDB[1], @@ -273,15 +272,66 @@ export const deleteLinodeInterface = (mockState: MockState) => [ const linodeId = Number(params.id); const interfaceId = Number(params.interfaceId); const linode = await mswDB.get('linodes', linodeId); - const linodeInterface = await mswDB.get('linodeInterfaces', interfaceId); + const linodeInterfaceTuple = await mswDB.get( + 'linodeInterfaces', + interfaceId + ); - if (!linode || !linodeInterface) { + if (!linode || !linodeInterfaceTuple) { return makeNotFoundResponse(); } - await mswDB.delete('linodeInterfaces', interfaceId, mockState); + const linodeInterface = linodeInterfaceTuple[1]; + + if (linodeInterface.vpc && linodeInterface.vpc.subnet_id) { + const subnetFromDB = await mswDB.get( + 'subnets', + linodeInterface.vpc.subnet_id + ); + const vpc = await mswDB.get( + 'vpcs', + linodeInterface.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 + ); + + if (subnetFromDB && vpc) { + // update VPC/subnet to remove interface + const updatedLinodeData = subnetFromDB[1].linodes.map((data) => { + return { + ...data, + interfaces: data.interfaces.filter( + (iface) => iface.id !== interfaceId && iface.config_id === null + ), + }; + }); + + const updatedSubnet = { + ...subnetFromDB[1], + linodes: updatedLinodeData, + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } + } - // todo connie: add vpc stuff... + await mswDB.delete('linodeInterfaces', interfaceId, mockState); queueEvents({ event: { @@ -339,7 +389,7 @@ export const updateLinodeInterface = (mockState: MockState) => [ entity: { id: interfaceId, label: linode.label, - type: 'subnets', + type: 'linodeInterface', url: `/v4beta/linodes/instances/${linode.id}/interfaces`, }, }, diff --git a/packages/manager/src/mocks/presets/crud/linodes.ts b/packages/manager/src/mocks/presets/crud/linodes.ts index 661ce0814fa..58cd3e50a59 100644 --- a/packages/manager/src/mocks/presets/crud/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/linodes.ts @@ -21,6 +21,11 @@ import { updateLinode, } from 'src/mocks/presets/crud/handlers/linodes/linodes'; +import { + appendConfigInterface, + deleteConfigInterface, +} from './handlers/linodes/configs'; + import type { MockPresetCrud } from 'src/mocks/types'; export const linodeCrudPreset: MockPresetCrud = { @@ -44,6 +49,8 @@ export const linodeCrudPreset: MockPresetCrud = { updateLinodeInterface, updateLinodeInterfaceSettings, getLinodeInterfaceFirewalls, + appendConfigInterface, + deleteConfigInterface, ], id: 'linodes:crud', label: 'Linode CRUD', From c01c0fdedd4d48cebaf85306ac0eef0e3bd1c521 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 15 Jul 2025 17:18:24 -0400 Subject: [PATCH 10/15] fix some bugs --- .../presets/crud/handlers/linodes/configs.ts | 71 +++++++++++++++++-- .../presets/crud/handlers/linodes/linodes.ts | 34 +-------- .../src/mocks/presets/crud/handlers/vpcs.ts | 1 + .../manager/src/mocks/presets/crud/linodes.ts | 2 + 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index da43d459c8c..f811acd8597 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -4,15 +4,78 @@ import { http } from 'msw'; import { makeNotFoundResponse, + makePaginatedResponse, makeResponse, } from 'src/mocks/utilities/response'; import { mswDB } from '../../../../indexedDB'; -import type { Interface } from '@linode/api-v4'; +import type { Config, Interface } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; import type { MockState } from 'src/mocks/types'; -import type { APIErrorResponse } from 'src/mocks/utilities/response'; +import type { + APIErrorResponse, + APIPaginatedResponse, +} from 'src/mocks/utilities/response'; + +export const getConfigs = () => [ + http.get( + '*/v4*/linode/instances/:id/configs', + async ({ + params, + request, + }): Promise< + StrictResponse> + > => { + const id = Number(params.id); + const linode = await mswDB.get('linodes', id); + const linodeConfigs = await mswDB.getAll('linodeConfigs'); + const configInterfaces = await mswDB.getAll('configInterfaces'); + + if (!linode || !linodeConfigs || !configInterfaces) { + return makeNotFoundResponse(); + } + + const configs = linodeConfigs + .filter((configTuple) => configTuple[0] === id) + .map((configTuple) => { + const interfacesForConfig = configInterfaces + .filter((interfaceTuple) => interfaceTuple[0] === configTuple[1].id) + .map((interfaceTuple) => interfaceTuple[1]); + return { ...configTuple[1], interfaces: interfacesForConfig }; + }); + + return makePaginatedResponse({ + data: configs, + request, + }); + } + ), + + http.get( + '*/v4*/linode/instances/:id/configs/:configId', + async ({ params }): Promise> => { + const id = Number(params.id); + const linode = await mswDB.get('linodes', id); + const configId = Number(params.configId); + const linodeConfig = await mswDB.get('linodeConfigs', configId); + const configInterfaces = await mswDB.getAll('configInterfaces'); + + if (!linode || !linodeConfig || !configInterfaces) { + return makeNotFoundResponse(); + } + + const interfaces = configInterfaces + .filter((interfaceTuple) => interfaceTuple[0] === configId) + .map((interfaceTuple) => interfaceTuple[1]); + + return makeResponse({ + ...linodeConfig[1], + interfaces, + }); + } + ), +]; export const appendConfigInterface = (mockState: MockState) => [ http.post( @@ -38,7 +101,7 @@ export const appendConfigInterface = (mockState: MockState) => [ const newlyAddedConfigInterface = await mswDB.add( 'configInterfaces', - [linode.id, configInterface], + [configId, configInterface], mockState ); @@ -101,7 +164,7 @@ export const appendConfigInterface = (mockState: MockState) => [ export const deleteConfigInterface = (mockState: MockState) => [ http.delete( - '*/v4*/linodes/instances/:id/configs/:configId/interfaces/:interfaceId', + '*/v4*/linode/instances/:id/configs/:configId/interfaces/:interfaceId', async ({ params }): Promise> => { const linodeId = Number(params.id); const configId = Number(params.configId); diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts index 92d5b89cb98..bc28ff50d84 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts @@ -23,7 +23,6 @@ import { import { mswDB } from '../../../../indexedDB'; import type { - Config, CreateLinodeInterfacePayload, Disk, Firewall, @@ -43,7 +42,7 @@ import type { export const getLinodes = () => [ http.get( - '*/v4/linode/instances', + '*/v4*/linode/instances', async ({ request, }): Promise< @@ -63,7 +62,7 @@ export const getLinodes = () => [ ), http.get( - '*/v4/linode/instances/:id', + '*/v4*/linode/instances/:id', async ({ params }): Promise> => { const id = Number(params.id); const linode = await mswDB.get('linodes', id); @@ -75,33 +74,6 @@ export const getLinodes = () => [ return makeResponse(linode); } ), - - http.get( - '*/v4/linode/instances/:id/configs', - async ({ - params, - request, - }): Promise< - StrictResponse> - > => { - const id = Number(params.id); - const linode = await mswDB.get('linodes', id); - const linodeConfigs = await mswDB.getAll('linodeConfigs'); - - if (!linode || !linodeConfigs) { - return makeNotFoundResponse(); - } - - const configs = linodeConfigs - .filter((configTuple) => configTuple[0] === id) - .map((configTuple) => configTuple[1]); - - return makePaginatedResponse({ - data: configs, - request, - }); - } - ), ]; export const addFirewallDevice = async (inputs: { @@ -159,7 +131,7 @@ export const addFirewallDevice = async (inputs: { }; export const createLinode = (mockState: MockState) => [ - http.post('*/v4/linode/instances', async ({ request }) => { + http.post('*/v4*/linode/instances', async ({ request }) => { const payload = await request.clone().json(); const payloadCopy = { ...payload }; diff --git a/packages/manager/src/mocks/presets/crud/handlers/vpcs.ts b/packages/manager/src/mocks/presets/crud/handlers/vpcs.ts index e5ba8e5baa5..d7ab3188de0 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/vpcs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/vpcs.ts @@ -120,6 +120,7 @@ export const createVPC = (mockState: MockState) => [ ...subnetPayload, created: DateTime.now().toISO(), linodes: [], + nodebalancers: [], updated: DateTime.now().toISO(), }); vpcSubnets.push(subnet); diff --git a/packages/manager/src/mocks/presets/crud/linodes.ts b/packages/manager/src/mocks/presets/crud/linodes.ts index 58cd3e50a59..fd9bfeb31d2 100644 --- a/packages/manager/src/mocks/presets/crud/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/linodes.ts @@ -24,6 +24,7 @@ import { import { appendConfigInterface, deleteConfigInterface, + getConfigs, } from './handlers/linodes/configs'; import type { MockPresetCrud } from 'src/mocks/types'; @@ -35,6 +36,7 @@ export const linodeCrudPreset: MockPresetCrud = { createLinode, updateLinode, deleteLinode, + getConfigs, getInterfaces, getLinodeStats, getLinodeDisks, From 0ba3699cffe5c6d5c3a40d0ef1e262634a6e0bff Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 15 Jul 2025 17:41:38 -0400 Subject: [PATCH 11/15] and a couple more --- .../src/mocks/presets/crud/handlers/linodes/interfaces.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index 4987af5142c..333703e13c6 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -267,7 +267,7 @@ export const createLinodeInterface = (mockState: MockState) => [ export const deleteLinodeInterface = (mockState: MockState) => [ http.delete( - '*/v4*/linodes/instances/:id/interfaces/:interfaceId', + '*/v4*/linode/instances/:id/interfaces/:interfaceId', async ({ params }): Promise> => { const linodeId = Number(params.id); const interfaceId = Number(params.interfaceId); @@ -354,7 +354,7 @@ export const deleteLinodeInterface = (mockState: MockState) => [ export const updateLinodeInterface = (mockState: MockState) => [ http.put( - '*/v4*/linodes/instances/:id/interfaces/:interfaceId', + '*/v4*/linode/instances/:id/interfaces/:interfaceId', async ({ params, request, @@ -509,7 +509,7 @@ export const upgradeToLinodeInterfaces = (mockState: MockState) => [ export const updateLinodeInterfaceSettings = () => [ http.put( - '*/v4*/linodes/instances/:id/interfaces/settings', + '*/v4*/linode/instances/:id/interfaces/settings', async ({ params, request, From dfc27e5ebf1394142280d28eca6a493c59983e02 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 16 Jul 2025 16:13:39 -0400 Subject: [PATCH 12/15] whack a mole --- .../presets/crud/handlers/linodes/configs.ts | 20 +++++++------- .../crud/handlers/linodes/interfaces.ts | 26 +++++++++++++------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index f811acd8597..28de462f1f8 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -194,15 +194,17 @@ export const deleteConfigInterface = (mockState: MockState) => [ if (subnetFromDB && vpc) { // update VPC/subnet to remove interface - const updatedLinodeData = subnetFromDB[1].linodes.map((data) => { - return { - ...data, - interfaces: data.interfaces.filter( - (iface) => - iface.id !== interfaceId && iface.config_id === configId - ), - }; - }); + const updatedLinodeData = subnetFromDB[1].linodes + .map((data) => { + return { + ...data, + interfaces: data.interfaces.filter( + (iface) => + iface.id !== interfaceId && iface.config_id === configId + ), + }; + }) + .filter((linode) => linode.interfaces.length > 0); const updatedSubnet = { ...subnetFromDB[1], diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index 333703e13c6..930ef269cfc 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -154,6 +154,9 @@ export const createLinodeInterface = (mockState: MockState) => [ if (payload.vpc) { linodeInterface = linodeInterfaceFactoryVPC.build({ ...payload, + default_route: { + ipv4: true, + }, created: DateTime.now().toISO(), updated: DateTime.now().toISO(), }); @@ -223,6 +226,10 @@ export const createLinodeInterface = (mockState: MockState) => [ } else { linodeInterface = linodeInterfaceFactoryPublic.build({ ...payload, + default_route: { + ipv4: true, + ipv6: true, + }, created: DateTime.now().toISO(), updated: DateTime.now().toISO(), }); @@ -295,14 +302,17 @@ export const deleteLinodeInterface = (mockState: MockState) => [ if (subnetFromDB && vpc) { // update VPC/subnet to remove interface - const updatedLinodeData = subnetFromDB[1].linodes.map((data) => { - return { - ...data, - interfaces: data.interfaces.filter( - (iface) => iface.id !== interfaceId && iface.config_id === null - ), - }; - }); + const updatedLinodeData = subnetFromDB[1].linodes + .map((data) => { + return { + ...data, + interfaces: data.interfaces.filter( + (iface) => + iface.id !== interfaceId && iface.config_id === null + ), + }; + }) + .filter((linode) => linode.interfaces.length > 0); const updatedSubnet = { ...subnetFromDB[1], From 386f3617cd19bd12a5d5c778578c5d35a00d5377 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 16 Jul 2025 17:38:53 -0400 Subject: [PATCH 13/15] firewalls --- .../crud/handlers/linodes/interfaces.ts | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index 930ef269cfc..ba2335295dd 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -35,6 +35,25 @@ import type { } from 'src/mocks/utilities/response'; export const getInterfaces = () => [ + // todo: connect this to the DB eventually + http.get( + '*/v4*/linode/instances/:id/interfaces/settings', + async ({ + params, + }): Promise> => { + const linodeId = Number(params.id); + const linode = await mswDB.get('linodes', linodeId); + + if (!linode) { + return makeNotFoundResponse(); + } + + const linodeSettings = linodeInterfaceSettingsFactory.build(); + + return makeResponse(linodeSettings); + } + ), + http.get( '*/v4*/linode/instances/:id/interfaces', async ({ @@ -79,26 +98,8 @@ export const getInterfaces = () => [ return makeResponse(linodeInterface[1]); } ), - - // todo: connect this to the DB eventually - http.get( - '*/v4*/linode/instances/:id/interfaces/settings', - async ({ - params, - }): Promise> => { - const linodeId = Number(params.id); - const linode = await mswDB.get('linodes', linodeId); - - if (!linode) { - return makeNotFoundResponse(); - } - - const linodeSettings = linodeInterfaceSettingsFactory.build(); - - return makeResponse(linodeSettings); - } - ), ]; + export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ http.get( '*/v4*/linode/instances/:id/interfaces/:interfaceId/firewalls', @@ -113,12 +114,12 @@ export const getLinodeInterfaceFirewalls = (mockState: MockState) => [ const linode = mockState.linodes.find( (stateLinode) => stateLinode.id === linodeId ); - const linodeInterface = mockState.linodes.find( - (stateLinode) => stateLinode.id === interfaceId + const linodeInterface = mockState.linodeInterfaces.find( + (stateLinode) => stateLinode[1].id === interfaceId ); const allFirewalls = await mswDB.getAll('firewalls'); - if (!linode || !linodeInterface || !allFirewalls) { + if (!linode || !linodeInterface || allFirewalls === undefined) { return makeNotFoundResponse(); } From e5d83cf58e91fa5ca47e0ee840f3aef13f83566b Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Jul 2025 10:20:49 -0400 Subject: [PATCH 14/15] consolidate shared logic --- .../presets/crud/handlers/linodes/configs.ts | 116 +++------------ .../crud/handlers/linodes/interfaces.ts | 113 +++------------ .../presets/crud/handlers/linodes/linodes.ts | 102 ++++--------- .../presets/crud/handlers/linodes/utils.ts | 134 ++++++++++++++++++ 4 files changed, 199 insertions(+), 266 deletions(-) create mode 100644 packages/manager/src/mocks/presets/crud/handlers/linodes/utils.ts diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts index 28de462f1f8..cdfcc933792 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -1,5 +1,4 @@ import { linodeConfigInterfaceFactory } from '@linode/utilities'; -import { DateTime } from 'luxon'; import { http } from 'msw'; import { @@ -9,6 +8,7 @@ import { } from 'src/mocks/utilities/response'; import { mswDB } from '../../../../indexedDB'; +import { addInterfaceToSubnet, removeInterfaceFromSubnet } from './utils'; import type { Config, Interface } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; @@ -106,55 +106,15 @@ export const appendConfigInterface = (mockState: MockState) => [ ); if (interfacePayload.purpose === 'vpc') { - // Update corresponding VPC when creating a VPC interface - const subnetFromDB = await mswDB.get( - 'subnets', - configInterface.subnet_id ?? -1 - ); - const vpc = await mswDB.get( - 'vpcs', - configInterface.vpc_id ?? subnetFromDB?.[0] ?? -1 - ); - - if (subnetFromDB && vpc) { - // update VPC/subnet to include this new interface - const updatedSubnet = { - ...subnetFromDB[1], - linodes: [ - ...subnetFromDB[1].linodes, - { - id: linode.id, - interfaces: [ - { - active: true, - config_id: configId, - id: newlyAddedConfigInterface[1].id, - }, - ], - }, - ], - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - } + await addInterfaceToSubnet({ + mockState, + interfaceId: newlyAddedConfigInterface[1].id, + isLinodeInterface: false, + linodeId: linode.id, + configId, + vpcId: configInterface.vpc_id, + subnetId: configInterface.subnet_id, + }); } return makeResponse(newlyAddedConfigInterface[1]); @@ -183,54 +143,14 @@ export const deleteConfigInterface = (mockState: MockState) => [ // if the config interface is part of a VPC, we must update the VPC as well const configInterface = configInterfaceTuple[1]; if (configInterface.purpose === 'vpc' && configInterface.subnet_id) { - const subnetFromDB = await mswDB.get( - 'subnets', - configInterface.subnet_id - ); - const vpc = await mswDB.get( - 'vpcs', - configInterface.vpc_id ?? subnetFromDB?.[0] ?? -1 - ); - - if (subnetFromDB && vpc) { - // update VPC/subnet to remove interface - const updatedLinodeData = subnetFromDB[1].linodes - .map((data) => { - return { - ...data, - interfaces: data.interfaces.filter( - (iface) => - iface.id !== interfaceId && iface.config_id === configId - ), - }; - }) - .filter((linode) => linode.interfaces.length > 0); - - const updatedSubnet = { - ...subnetFromDB[1], - linodes: updatedLinodeData, - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - } + await removeInterfaceFromSubnet({ + mockState, + configId, + interfaceId: configInterface.id, + isLinodeInterface: false, + subnetId: configInterface.subnet_id, + vpcId: configInterface.vpc_id, + }); } await mswDB.delete('configInterfaces', interfaceId, mockState); diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts index ba2335295dd..447c1bebd43 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -16,6 +16,7 @@ import { import { mswDB } from '../../../../indexedDB'; import { addFirewallDevice } from './linodes'; +import { addInterfaceToSubnet, removeInterfaceFromSubnet } from './utils'; import type { Config, @@ -169,55 +170,14 @@ export const createLinodeInterface = (mockState: MockState) => [ ); // Update corresponding VPC when creating a VPC Linode Interface - const subnetFromDB = await mswDB.get( - 'subnets', - payload.vpc.subnet_id ?? -1 - ); - const vpc = await mswDB.get( - 'vpcs', - payload.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 - ); - - if (subnetFromDB && vpc) { - // update VPC/subnet to include this new interface - const updatedSubnet = { - ...subnetFromDB[1], - linodes: [ - ...subnetFromDB[1].linodes, - { - id: linode.id, - interfaces: [ - { - active: true, - config_id: null, - // ensure interface ID in subnet matches interface ID in DB - id: createdInterface[1].id, - }, - ], - }, - ], - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - } + await addInterfaceToSubnet({ + interfaceId: createdInterface[1].id, + isLinodeInterface: true, + linodeId: linode.id, + vpcId: payload.vpc.vpc_id, + subnetId: payload.vpc.subnet_id, + mockState, + }); } else if (payload.vlan) { linodeInterface = linodeInterfaceFactoryVlan.build({ ...payload, @@ -292,54 +252,13 @@ export const deleteLinodeInterface = (mockState: MockState) => [ const linodeInterface = linodeInterfaceTuple[1]; if (linodeInterface.vpc && linodeInterface.vpc.subnet_id) { - const subnetFromDB = await mswDB.get( - 'subnets', - linodeInterface.vpc.subnet_id - ); - const vpc = await mswDB.get( - 'vpcs', - linodeInterface.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 - ); - - if (subnetFromDB && vpc) { - // update VPC/subnet to remove interface - const updatedLinodeData = subnetFromDB[1].linodes - .map((data) => { - return { - ...data, - interfaces: data.interfaces.filter( - (iface) => - iface.id !== interfaceId && iface.config_id === null - ), - }; - }) - .filter((linode) => linode.interfaces.length > 0); - - const updatedSubnet = { - ...subnetFromDB[1], - linodes: updatedLinodeData, - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - } + await removeInterfaceFromSubnet({ + mockState, + interfaceId, + isLinodeInterface: true, + subnetId: linodeInterface.vpc.subnet_id, + vpcId: linodeInterface.vpc.vpc_id, + }); } await mswDB.delete('linodeInterfaces', interfaceId, mockState); diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts index bc28ff50d84..d127a10010d 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts @@ -21,6 +21,7 @@ import { } from 'src/mocks/utilities/response'; import { mswDB } from '../../../../indexedDB'; +import { addInterfaceToSubnet } from './utils'; import type { CreateLinodeInterfacePayload, @@ -178,80 +179,39 @@ export const createLinode = (mockState: MockState) => [ (iface: CreateLinodeInterfacePayload) => iface.vpc ); - const subnetFromDB = await mswDB.get( - 'subnets', - vpcIfacePayload.vpc.subnet_id ?? -1 - ); - const vpc = await mswDB.get( - 'vpcs', - vpcIfacePayload.vpc.vpc_id ?? subnetFromDB?.[0] ?? -1 + const vpcInterface = linodeInterfaceFactoryVPC.build({ + ...vpcIfacePayload, + default_route: { + ipv4: true, + }, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + + const createdVPCInterface = await mswDB.add( + 'linodeInterfaces', + [linode.id, vpcInterface], + mockState ); - if (subnetFromDB && vpc) { - const vpcInterface = linodeInterfaceFactoryVPC.build({ - ...vpcIfacePayload, - default_route: { - ipv4: true, - }, - created: DateTime.now().toISO(), - updated: DateTime.now().toISO(), - }); + await addInterfaceToSubnet({ + mockState, + interfaceId: createdVPCInterface[1].id, + linodeId: linode.id, + isLinodeInterface: true, + vpcId: vpcIfacePayload.vpc.vpc_id, + subnetId: vpcIfacePayload.vpc.subnet_id, + }); - const createdVPCInterface = await mswDB.add( - 'linodeInterfaces', - [linode.id, vpcInterface], - mockState - ); - - // update VPC/subnet to include this new interface - const updatedSubnet = { - ...subnetFromDB[1], - linodes: [ - ...subnetFromDB[1].linodes, - { - id: linode.id, - interfaces: [ - { - active: true, - config_id: null, - // ensure interface ID in subnet matches interface ID in DB - id: createdVPCInterface[1].id, - }, - ], - }, - ], - updated: DateTime.now().toISO(), - }; - - const updatedVPC = { - ...vpc, - subnets: vpc.subnets.map((subnet) => { - if (subnet.id === subnetFromDB[1].id) { - return updatedSubnet; - } - - return subnet; - }), - }; - - await mswDB.update( - 'subnets', - subnetFromDB[1].id, - [vpc.id, updatedSubnet], - mockState - ); - await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); - - // if firewall given in interface payload, add a device - if (vpcIfacePayload.firewall_id) { - await addFirewallDevice({ - entityId: vpcInterface.id, - entityLabel: linode.label, - firewallId: vpcIfacePayload.firewall_id, - interfaceType: 'linode_interface', - mockState, - }); - } + // if firewall given in interface payload, add a device + if (vpcIfacePayload.firewall_id) { + await addFirewallDevice({ + entityId: vpcInterface.id, + entityLabel: linode.label, + firewallId: vpcIfacePayload.firewall_id, + interfaceType: 'linode_interface', + mockState, + }); } } diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes/utils.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes/utils.ts new file mode 100644 index 00000000000..c9f9ae0919b --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/utils.ts @@ -0,0 +1,134 @@ +import { DateTime } from 'luxon'; + +import { mswDB } from '../../../../indexedDB'; + +import type { MockState } from 'src/mocks/types'; +// shared functionality between different linode related handlers + +export const addInterfaceToSubnet = async (inputs: { + configId?: number; + interfaceId: number; + isLinodeInterface: boolean; + linodeId: number; + mockState: MockState; + subnetId?: null | number; + vpcId?: null | number; +}) => { + const { + mockState, + configId, + interfaceId, + isLinodeInterface, + linodeId, + subnetId, + vpcId, + } = inputs; + // Update corresponding VPC when creating a VPC Linode Interface + const subnetFromDB = await mswDB.get('subnets', subnetId ?? -1); + const vpc = await mswDB.get('vpcs', vpcId ?? subnetFromDB?.[0] ?? -1); + + if (subnetFromDB && vpc) { + // update VPC/subnet to include this new interface + const updatedSubnet = { + ...subnetFromDB[1], + linodes: [ + ...subnetFromDB[1].linodes, + { + id: linodeId, + interfaces: [ + { + active: true, + config_id: isLinodeInterface ? null : (configId ?? -1), + // ensure interface ID in subnet matches interface ID in DB + id: interfaceId, + }, + ], + }, + ], + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } +}; + +export const removeInterfaceFromSubnet = async (inputs: { + configId?: number; + interfaceId: number; + isLinodeInterface: boolean; + mockState: MockState; + subnetId: number; + vpcId?: null | number; +}) => { + const { + mockState, + configId, + interfaceId, + isLinodeInterface, + subnetId, + vpcId, + } = inputs; + + const subnetFromDB = await mswDB.get('subnets', subnetId); + const vpc = await mswDB.get('vpcs', vpcId ?? subnetFromDB?.[0] ?? -1); + + if (subnetFromDB && vpc) { + // update VPC/subnet to remove interface + const updatedLinodeData = subnetFromDB[1].linodes + .map((data) => { + return { + ...data, + interfaces: data.interfaces.filter( + (iface) => + iface.id !== interfaceId && + (isLinodeInterface + ? iface.config_id === null + : iface.config_id === configId) + ), + }; + }) + .filter((linode) => linode.interfaces.length > 0); + + const updatedSubnet = { + ...subnetFromDB[1], + linodes: updatedLinodeData, + updated: DateTime.now().toISO(), + }; + + const updatedVPC = { + ...vpc, + subnets: vpc.subnets.map((subnet) => { + if (subnet.id === subnetFromDB[1].id) { + return updatedSubnet; + } + + return subnet; + }), + }; + + await mswDB.update( + 'subnets', + subnetFromDB[1].id, + [vpc.id, updatedSubnet], + mockState + ); + await mswDB.update('vpcs', vpc.id, updatedVPC, mockState); + } +}; From 66c64b22f8fec92b12f38dd4916597cf5e3ae41f Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Jul 2025 11:03:34 -0400 Subject: [PATCH 15/15] Added changeset: Add MSW crud support for assigning Linodes to a VPC --- .../.changeset/pr-12503-tech-stories-1752764614523.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-12503-tech-stories-1752764614523.md diff --git a/packages/manager/.changeset/pr-12503-tech-stories-1752764614523.md b/packages/manager/.changeset/pr-12503-tech-stories-1752764614523.md new file mode 100644 index 00000000000..f8e525fe92e --- /dev/null +++ b/packages/manager/.changeset/pr-12503-tech-stories-1752764614523.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Add MSW crud support for assigning Linodes to a VPC ([#12503](https://github.com/linode/manager/pull/12503))