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)) 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 new file mode 100644 index 00000000000..cdfcc933792 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/configs.ts @@ -0,0 +1,161 @@ +import { linodeConfigInterfaceFactory } from '@linode/utilities'; +import { http } from 'msw'; + +import { + makeNotFoundResponse, + makePaginatedResponse, + makeResponse, +} 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'; +import type { MockState } from 'src/mocks/types'; +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( + '*/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 interfacePayload = await request.clone().json(); + const configInterface = linodeConfigInterfaceFactory.build({ + ...interfacePayload, + active: true, + }); + + const newlyAddedConfigInterface = await mswDB.add( + 'configInterfaces', + [configId, configInterface], + mockState + ); + + if (interfacePayload.purpose === 'vpc') { + 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]); + } + ), +]; + +export const deleteConfigInterface = (mockState: MockState) => [ + http.delete( + '*/v4*/linode/instances/:id/configs/:configId/interfaces/:interfaceId', + 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 || !configInterfaceTuple) { + return makeNotFoundResponse(); + } + + // 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) { + await removeInterfaceFromSubnet({ + mockState, + configId, + interfaceId: configInterface.id, + isLinodeInterface: false, + subnetId: configInterface.subnet_id, + vpcId: configInterface.vpc_id, + }); + } + + await mswDB.delete('configInterfaces', interfaceId, mockState); + + return makeResponse({}); + } + ), +]; 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..447c1bebd43 --- /dev/null +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes/interfaces.ts @@ -0,0 +1,463 @@ +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 { addInterfaceToSubnet, removeInterfaceFromSubnet } from './utils'; + +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 = () => [ + // 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 ({ + 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]); + } + ), +]; + +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.linodeInterfaces.find( + (stateLinode) => stateLinode[1].id === interfaceId + ); + const allFirewalls = await mswDB.getAll('firewalls'); + + if (!linode || !linodeInterface || allFirewalls === undefined) { + 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, + default_route: { + ipv4: true, + }, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + + const createdInterface = await mswDB.add( + 'linodeInterfaces', + [linodeId, linodeInterface], + mockState + ); + + // Update corresponding VPC when creating a VPC Linode Interface + 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, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } else { + linodeInterface = linodeInterfaceFactoryPublic.build({ + ...payload, + default_route: { + ipv4: true, + ipv6: true, + }, + created: DateTime.now().toISO(), + updated: DateTime.now().toISO(), + }); + } + + if (!payload.vpc) { + 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*/linode/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 linodeInterfaceTuple = await mswDB.get( + 'linodeInterfaces', + interfaceId + ); + + if (!linode || !linodeInterfaceTuple) { + return makeNotFoundResponse(); + } + + const linodeInterface = linodeInterfaceTuple[1]; + + if (linodeInterface.vpc && linodeInterface.vpc.subnet_id) { + await removeInterfaceFromSubnet({ + mockState, + interfaceId, + isLinodeInterface: true, + subnetId: linodeInterface.vpc.subnet_id, + vpcId: linodeInterface.vpc.vpc_id, + }); + } + + 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*/linode/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: 'linodeInterface', + 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*/linode/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 51% rename from packages/manager/src/mocks/presets/crud/handlers/linodes.ts rename to packages/manager/src/mocks/presets/crud/handlers/linodes/linodes.ts index a8ed0d083af..d127a10010d 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,25 +20,19 @@ import { makeResponse, } from 'src/mocks/utilities/response'; -import { mswDB } from '../../../indexedDB'; +import { mswDB } from '../../../../indexedDB'; +import { addInterfaceToSubnet } from './utils'; import type { - Config, CreateLinodeInterfacePayload, Disk, Firewall, FirewallDeviceEntityType, - 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'; @@ -50,7 +43,7 @@ import type { export const getLinodes = () => [ http.get( - '*/v4/linode/instances', + '*/v4*/linode/instances', async ({ request, }): Promise< @@ -70,7 +63,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); @@ -82,100 +75,9 @@ export const getLinodes = () => [ return makeResponse(linode); } ), - - 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 ({ - 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, - }); - } - ), ]; -const addFirewallDevice = async (inputs: { +export const addFirewallDevice = async (inputs: { entityId: number; entityLabel: string; firewallId: number; @@ -230,7 +132,7 @@ 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 }; @@ -277,78 +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, + }); - // 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.add( - 'linodeInterfaces', - [linode.id, vpcInterface], - mockState - ); - 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, + }); } } @@ -400,6 +263,7 @@ export const createLinode = (mockState: MockState) => [ ); } } else { + // TODO UPDATE THIS const linodeConfig = configFactory.build({ created: DateTime.now().toISO(), }); @@ -737,315 +601,4 @@ export const shutDownLinode = (mockState: MockState) => [ ), ]; -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(), - }); - } 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); - } - ), -]; - -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); - } - ), -]; - // TODO: ad more handlers (reboot, clone, resize, rebuild, rescue, migrate...) as needed 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); + } +}; 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 4a4a2247c9f..fd9bfeb31d2 100644 --- a/packages/manager/src/mocks/presets/crud/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/linodes.ts @@ -1,22 +1,31 @@ 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 { + appendConfigInterface, + deleteConfigInterface, + getConfigs, +} from './handlers/linodes/configs'; import type { MockPresetCrud } from 'src/mocks/types'; @@ -27,6 +36,8 @@ export const linodeCrudPreset: MockPresetCrud = { createLinode, updateLinode, deleteLinode, + getConfigs, + getInterfaces, getLinodeStats, getLinodeDisks, getLinodeFirewalls, @@ -40,6 +51,8 @@ export const linodeCrudPreset: MockPresetCrud = { updateLinodeInterface, updateLinodeInterfaceSettings, getLinodeInterfaceFirewalls, + appendConfigInterface, + deleteConfigInterface, ], id: 'linodes:crud', label: 'Linode CRUD', 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[];