Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [M3-9088] - Add and update /v4/networking endpoints and types for Linode Interfaces #11559

Merged
merged 11 commits into from
Jan 27, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

Add and update `/v4/networking` endpoints and types for Linode Interfaces ([#11559](https://github.com/linode/manager/pull/11559))
36 changes: 33 additions & 3 deletions packages/api-v4/src/firewalls/firewalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import {
CreateFirewallSchema,
FirewallDeviceSchema,
UpdateFirewallSchema,
UpdateFirewallSettingsSchema,
} from '@linode/validation/lib/firewalls.schema';
import {
CreateFirewallPayload,
Firewall,
FirewallDevice,
FirewallDevicePayload,
FirewallRules,
FirewallSettings,
FirewallTemplate,
FirewallTemplateSlug,
UpdateFirewallPayload,
UpdateFirewallRules,
UpdateFirewallSettings,
} from './types';

/**
Expand Down Expand Up @@ -150,7 +155,10 @@ export const getFirewallRules = (
* Updates the inbound and outbound Rules for a Firewall. Using this endpoint will
* replace all of a Firewall's ruleset with the Rules specified in your request.
*/
export const updateFirewallRules = (firewallID: number, data: FirewallRules) =>
export const updateFirewallRules = (
firewallID: number,
data: UpdateFirewallRules
) =>
Request<FirewallRules>(
setMethod('PUT'),
setData(data), // Validation is too complicated for these; leave it to the API.
Expand Down Expand Up @@ -245,6 +253,29 @@ export const deleteFirewallDevice = (firewallID: number, deviceID: number) =>
)
);

/**
* getFirewallSettings
*
* Returns current interface default firewall settings
*/
export const getFirewallSettings = () =>
Request<FirewallSettings>(
setMethod('GET'),
setURL(`${BETA_API_ROOT}/networking/firewalls/settings`)
);

/**
* updateFirewallSettings
*
* Update which firewalls should be the interface default firewalls
*/
export const updateFirewallSettings = (data: UpdateFirewallSettings) =>
Request<FirewallSettings>(
setMethod('PUT'),
setURL(`${BETA_API_ROOT}/networking/firewalls/settings`),
setData(data, UpdateFirewallSettingsSchema)
);

// #region Templates

/**
Expand All @@ -262,9 +293,8 @@ export const getTemplates = () =>
* getTemplate
*
* Get a specific firewall template by its slug.
*
*/
export const getTemplate = (templateSlug: string) =>
export const getTemplate = (templateSlug: FirewallTemplateSlug) =>
Copy link
Contributor Author

@coliu-akamai coliu-akamai Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two endpoints (getTemplates and getTemplate) were added during the Securing VMs project (see internal ticket - I linked the spec there). I updated the templateSlug type to be more specific - I believe these are the three templates that exist so far, but am not sure. Since there may be a possibility for more slugs I'm not aware of, happy to switch back to string too

I've currently typed it as
export type FirewallTemplateSlug = 'akamai-non-prod' | 'vpc' | 'public'
See internal comments on spec about some naming updates - when testing the endpoint, I saw that vpc and public are the returned slugs

Request<FirewallTemplate>(
setMethod('GET'),
setURL(
Expand Down
44 changes: 34 additions & 10 deletions packages/api-v4/src/firewalls/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export type FirewallStatus = 'enabled' | 'disabled' | 'deleted';

export type FirewallRuleProtocol = 'ALL' | 'TCP' | 'UDP' | 'ICMP' | 'IPENCAP';

export type FirewallDeviceEntityType = 'linode' | 'nodebalancer';
export type FirewallDeviceEntityType = 'linode' | 'nodebalancer' | 'interface';

export type FirewallPolicyType = 'ACCEPT' | 'DROP';

Expand All @@ -14,21 +14,27 @@ export interface Firewall {
rules: FirewallRules;
created: string;
updated: string;
entities: {
id: number;
type: FirewallDeviceEntityType;
label: string;
url: string;
}[];
Comment on lines -17 to -22
Copy link
Contributor Author

@coliu-akamai coliu-akamai Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replacing this with FirewallDeviceEntity since it already exists

export interface FirewallDeviceEntity {
  id: number;
  type: FirewallDeviceEntityType;
  label: string;
  url: string;
}

entities: FirewallDeviceEntity[];
}

export interface FirewallRules {
fingerprint: string;
inbound?: FirewallRuleType[] | null;
outbound?: FirewallRuleType[] | null;
inbound_policy: FirewallPolicyType;
outbound_policy: FirewallPolicyType;
version: number;
}

export type UpdateFirewallRules = Omit<
FirewallRules,
'fingerprint' | 'version'
>;

// @TODO Linode Interfaces - follow up on if FirewallTemplate rules have the fingerprint / version fields.
// FirewallRules do, but the objects returned from the getFirewallTemplate requests don't
export type FirewallTemplateRules = UpdateFirewallRules;

export interface FirewallRuleType {
label?: string | null;
description?: string | null;
Expand All @@ -55,18 +61,21 @@ export interface FirewallDevice {
entity: FirewallDeviceEntity;
}

export type FirewallTemplateSlug = 'akamai-non-prod' | 'vpc' | 'public';

export interface FirewallTemplate {
slug: string;
rules: FirewallRules;
slug: FirewallTemplateSlug;
rules: FirewallTemplateRules;
}

export interface CreateFirewallPayload {
label?: string;
tags?: string[];
rules: FirewallRules;
rules: UpdateFirewallRules;
devices?: {
linodes?: number[];
nodebalancers?: number[];
interfaces?: number[];
};
}

Expand All @@ -80,3 +89,18 @@ export interface FirewallDevicePayload {
id: number;
type: FirewallDeviceEntityType;
}

export interface DefaultFirewallIDs {
interface_public: number;
interface_vpc: number;
linode: number;
nodebalancer: number;
}

export interface FirewallSettings {
default_firewall_ids: DefaultFirewallIDs;
}

export interface UpdateFirewallSettings {
default_firewall_ids: Partial<DefaultFirewallIDs>;
}
6 changes: 6 additions & 0 deletions packages/api-v4/src/networking/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ export interface IPAddress {
public: boolean;
rdns: string | null;
linode_id: number;
interface_id: number | null;
region: string;
vpc_nat_1_1?: {
address: string;
subnet_id: number;
vpc_id: number;
} | null;
Comment on lines +12 to +16
Copy link
Contributor Author

@coliu-akamai coliu-akamai Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding this field (looks like we were missing it previously). It got returned either as the object or a null value (or was omitted) when I tested the endpoint

}

export interface IPRangeBaseData {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Fix type errors that result from changes to `/v4/networking` endpoints ([#11559](https://github.com/linode/manager/pull/11559))
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,11 @@ componentTests('Firewall Rules Table', (mount) => {
mount(
<FirewallRulesLanding
rules={{
fingerprint: '8a545843',
inbound: mockInboundRules,
inbound_policy: 'ACCEPT',
outbound_policy: 'DROP',
version: 1,
}}
disabled={false}
firewallID={randomNumber()}
Expand Down Expand Up @@ -478,9 +480,11 @@ componentTests('Firewall Rules Table', (mount) => {
mount(
<FirewallRulesLanding
rules={{
fingerprint: '8a545843',
inbound_policy: 'ACCEPT',
outbound: mockOutboundRules,
outbound_policy: 'DROP',
version: 1,
}}
disabled={false}
firewallID={randomNumber()}
Expand Down Expand Up @@ -547,9 +551,11 @@ componentTests('Firewall Rules Table', (mount) => {
mount(
<FirewallRulesLanding
rules={{
fingerprint: '8a545843',
inbound: mockInboundRules,
inbound_policy: 'ACCEPT',
outbound_policy: 'DROP',
version: 1,
}}
disabled={false}
firewallID={randomNumber()}
Expand All @@ -576,9 +582,11 @@ componentTests('Firewall Rules Table', (mount) => {
mount(
<FirewallRulesLanding
rules={{
fingerprint: '8a545843',
inbound_policy: 'ACCEPT',
outbound: mockOutboundRules,
outbound_policy: 'DROP',
version: 1,
}}
disabled={false}
firewallID={randomNumber()}
Expand Down
4 changes: 4 additions & 0 deletions packages/manager/src/__data__/firewalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const firewall: Firewall = {
id: 1,
label: 'my-firewall',
rules: {
fingerprint: '8a545843',
inbound: [
{
action: 'ACCEPT',
Expand All @@ -34,6 +35,7 @@ export const firewall: Firewall = {
},
],
outbound_policy: 'DROP',
version: 1,
},
status: 'enabled',
tags: [],
Expand All @@ -53,6 +55,7 @@ export const firewall2: Firewall = {
id: 2,
label: 'zzz',
rules: {
fingerprint: '8a545843',
inbound: [],
inbound_policy: 'DROP',
outbound: [
Expand All @@ -68,6 +71,7 @@ export const firewall2: Firewall = {
},
],
outbound_policy: 'DROP',
version: 1,
},
status: 'disabled',
tags: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import userEvent from '@testing-library/user-event';
import React from 'react';

import { firewallFactory, firewallTemplateFactory } from 'src/factories';
import {
firewallFactory,
firewallRuleFactory,
firewallTemplateFactory,
} from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';
Expand All @@ -11,7 +15,23 @@ import { GenerateFirewallDialog } from './GenerateFirewallDialog';
describe('GenerateFirewallButton', () => {
it('Can successfully generate a firewall', async () => {
const firewalls = firewallFactory.buildList(2);
const template = firewallTemplateFactory.build();
const template = firewallTemplateFactory.build({
rules: {
// due to an updated firewallTemplateFactory, we need to specify values for this test
inbound: [
firewallRuleFactory.build({
description: 'firewall-rule-1 description',
label: 'firewall-rule-1',
}),
],
outbound: [
firewallRuleFactory.build({
description: 'firewall-rule-2 description',
label: 'firewall-rule-2',
}),
],
},
});
const createFirewallCallback = vi.fn();
const onClose = vi.fn();
const onFirewallGenerated = vi.fn();
Expand Down Expand Up @@ -55,14 +75,14 @@ describe('GenerateFirewallButton', () => {
expect(onFirewallGenerated).toHaveBeenCalledWith(
expect.objectContaining({
label: `${template.slug}-1`,
rules: template.rules,
rules: { ...template.rules, fingerprint: '8a545843', version: 1 },
})
);

expect(createFirewallCallback).toHaveBeenCalledWith(
expect.objectContaining({
label: `${template.slug}-1`,
rules: template.rules,
rules: { ...template.rules, fingerprint: '8a545843', version: 1 },
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { firewallQueries } from 'src/queries/firewalls';
import { useCreateFirewall } from 'src/queries/firewalls';

import type { DialogState } from './GenerateFirewallDialog';
import type { CreateFirewallPayload, Firewall } from '@linode/api-v4';
import type {
CreateFirewallPayload,
Firewall,
FirewallTemplateSlug,
} from '@linode/api-v4';
import type { QueryClient } from '@tanstack/react-query';

export const useCreateFirewallFromTemplate = (options: {
onFirewallGenerated?: (firewall: Firewall) => void;
setDialogState: (state: DialogState) => void;
templateSlug: string;
templateSlug: FirewallTemplateSlug;
}) => {
const { onFirewallGenerated, setDialogState, templateSlug } = options;
const queryClient = useQueryClient();
Expand Down Expand Up @@ -44,7 +48,7 @@ export const useCreateFirewallFromTemplate = (options: {
const createFirewallFromTemplate = async (options: {
createFirewall: (firewall: CreateFirewallPayload) => Promise<Firewall>;
queryClient: QueryClient;
templateSlug: string;
templateSlug: FirewallTemplateSlug;
updateProgress: (progress: number | undefined) => void;
}): Promise<Firewall> => {
const { createFirewall, queryClient, templateSlug, updateProgress } = options;
Expand All @@ -66,7 +70,7 @@ const createFirewallFromTemplate = async (options: {
};

const getUniqueFirewallLabel = (
templateSlug: string,
templateSlug: FirewallTemplateSlug,
firewalls: Firewall[]
) => {
let iterator = 1;
Expand All @@ -79,7 +83,10 @@ const getUniqueFirewallLabel = (
return firewallLabelFromSlug(templateSlug, iterator);
};

const firewallLabelFromSlug = (slug: string, iterator: number) => {
const firewallLabelFromSlug = (
slug: FirewallTemplateSlug,
iterator: number
) => {
const MAX_LABEL_LENGTH = 32;
const iteratorSuffix = `-${iterator}`;
return (
Expand Down
Loading
Loading