1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

feat(ingress): autodetect ingress controllers EE-673 (#7712)

This commit is contained in:
Dakota Walsh 2022-09-27 08:43:24 +13:00 committed by GitHub
parent c96551e410
commit 89eda13eb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1252 additions and 1047 deletions

View file

@ -0,0 +1,221 @@
import { useState } from 'react';
import { confirmWarn } from '@/portainer/services/modal.service/confirm';
import { Datatable } from '@@/datatables';
import { Button, ButtonGroup } from '@@/buttons';
import { Icon } from '@@/Icon';
import { IngressControllerClassMap } from '../types';
import { useColumns } from './columns';
import { createStore } from './datatable-store';
const useStore = createStore('ingressClasses');
interface Props {
onChangeAvailability: (
controllerClassMap: IngressControllerClassMap[]
) => void; // angular function to save the ingress class list
description: string;
ingressControllers: IngressControllerClassMap[] | undefined;
noIngressControllerLabel: string;
view: string;
}
export function IngressClassDatatable({
onChangeAvailability,
description,
ingressControllers,
noIngressControllerLabel,
view,
}: Props) {
const [ingControllerFormValues, setIngControllerFormValues] =
useState(ingressControllers);
const settings = useStore();
const columns = useColumns();
return (
<div className="-mx-[15px]">
<Datatable
dataset={ingControllerFormValues || []}
storageKey="ingressClasses"
columns={columns}
settingsStore={settings}
isLoading={!ingControllerFormValues}
emptyContentLabel={noIngressControllerLabel}
titleOptions={{
icon: 'database',
title: 'Ingress controllers',
featherIcon: true,
}}
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
description={renderIngressClassDescription()}
/>
</div>
);
function renderTableActions(selectedRows: IngressControllerClassMap[]) {
return (
<div className="flex items-start">
<ButtonGroup>
<Button
disabled={
selectedRows.filter((row) => row.Availability === true).length ===
0
}
color="dangerlight"
size="small"
onClick={() =>
updateIngressControllers(
selectedRows,
ingControllerFormValues || [],
false
)
}
>
Disallow selected
</Button>
<Button
disabled={
selectedRows.filter((row) => row.Availability === false)
.length === 0
}
color="default"
size="small"
onClick={() =>
updateIngressControllers(
selectedRows,
ingControllerFormValues || [],
true
)
}
>
Allow selected
</Button>
</ButtonGroup>
</div>
);
}
function renderIngressClassDescription() {
return (
<div className="flex flex-col !text-xs text-muted w-full">
<div className="mt-1">{description}</div>
{ingressControllers &&
ingControllerFormValues &&
isUnsavedChanges(ingressControllers, ingControllerFormValues) && (
<span className="flex items-center text-warning mt-1">
<Icon icon="alert-triangle" feather className="!mr-1" />
<span className="text-warning">Unsaved changes.</span>
</span>
)}
</div>
);
}
function updateIngressControllers(
selectedRows: IngressControllerClassMap[],
ingControllerFormValues: IngressControllerClassMap[],
availability: boolean
) {
const updatedIngressControllers = getUpdatedIngressControllers(
selectedRows,
ingControllerFormValues || [],
availability
);
if (ingressControllers && ingressControllers.length) {
const newAllowed = updatedIngressControllers.map(
(ingController) => ingController.Availability
);
if (view === 'namespace') {
setIngControllerFormValues(updatedIngressControllers);
onChangeAvailability(updatedIngressControllers);
return;
}
const usedControllersToDisallow = ingressControllers.filter(
(ingController, index) => {
// if any of the current controllers are allowed, and are used, then become disallowed, then add the controller to a new list
if (
ingController.Availability &&
ingController.Used &&
!newAllowed[index]
) {
return true;
}
return false;
}
);
if (usedControllersToDisallow.length > 0) {
const usedControllerHtmlListItems = usedControllersToDisallow.map(
(controller) => `<li>${controller.ClassName}</li>`
);
const usedControllerHtmlList = `<ul class="ml-6">${usedControllerHtmlListItems.join(
''
)}</ul>`;
confirmWarn({
title: 'Disallow in-use ingress controllers?',
message: `
<div>
<p>There are ingress controllers you want to disallow that are in use:</p>
${usedControllerHtmlList}
<p>No new ingress rules can be created for the disallowed controllers.</p>
</div>`,
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
},
confirm: {
label: 'Disallow',
className: 'btn-warning',
},
},
callback: (confirmed) => {
if (confirmed) {
setIngControllerFormValues(updatedIngressControllers);
onChangeAvailability(updatedIngressControllers);
}
},
});
return;
}
setIngControllerFormValues(updatedIngressControllers);
onChangeAvailability(updatedIngressControllers);
}
}
}
function isUnsavedChanges(
oldIngressControllers: IngressControllerClassMap[],
newIngressControllers: IngressControllerClassMap[]
) {
for (let i = 0; i < oldIngressControllers.length; i += 1) {
if (
oldIngressControllers[i].Availability !==
newIngressControllers[i].Availability
) {
return true;
}
}
return false;
}
function getUpdatedIngressControllers(
selectedRows: IngressControllerClassMap[],
allRows: IngressControllerClassMap[],
allow: boolean
) {
const selectedRowClassNames = selectedRows.map((row) => row.ClassName);
const updatedIngressControllers = allRows?.map((row) => {
if (selectedRowClassNames.includes(row.ClassName)) {
return { ...row, Availability: allow };
}
return row;
});
return updatedIngressControllers;
}

View file

@ -0,0 +1,11 @@
import { Environment } from '@/portainer/environments/types';
import { createRowContext } from '@@/datatables/RowContext';
interface RowContextState {
environment: Environment;
}
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
export { RowProvider, useRowContext };

View file

@ -0,0 +1,27 @@
import { CellProps, Column } from 'react-table';
import { Badge } from '@@/Badge';
import { Icon } from '@@/Icon';
import type { IngressControllerClassMap } from '../../types';
export const availability: Column<IngressControllerClassMap> = {
Header: 'Availability',
accessor: 'Availability',
Cell: AvailailityCell,
id: 'availability',
disableFilters: true,
canHide: true,
sortInverted: true,
sortType: 'basic',
Filter: () => null,
};
function AvailailityCell({ value }: CellProps<IngressControllerClassMap>) {
return (
<Badge type={value ? 'success' : 'danger'}>
<Icon icon={value ? 'check' : 'x'} feather className="!mr-1" />
{value ? 'Allowed' : 'Disallowed'}
</Badge>
);
}

View file

@ -0,0 +1,9 @@
import { useMemo } from 'react';
import { availability } from './availability';
import { type } from './type';
import { name } from './name';
export function useColumns() {
return useMemo(() => [name, type, availability], []);
}

View file

@ -0,0 +1,25 @@
import { CellProps, Column } from 'react-table';
import { Badge } from '@@/Badge';
import type { IngressControllerClassMap } from '../../types';
export const name: Column<IngressControllerClassMap> = {
Header: 'Ingress class',
accessor: 'ClassName',
Cell: NameCell,
id: 'name',
disableFilters: true,
canHide: true,
Filter: () => null,
sortType: 'string',
};
function NameCell({ row }: CellProps<IngressControllerClassMap>) {
return (
<span className="flex flex-nowrap">
{row.original.ClassName}
{row.original.New && <Badge className="ml-1">Newly detected</Badge>}
</span>
);
}

View file

@ -0,0 +1,14 @@
import { CellProps, Column } from 'react-table';
import type { IngressControllerClassMap } from '../../types';
export const type: Column<IngressControllerClassMap> = {
Header: 'Ingress controller type',
accessor: 'Type',
Cell: ({ row }: CellProps<IngressControllerClassMap>) =>
row.original.Type || '-',
id: 'type',
disableFilters: true,
canHide: true,
Filter: () => null,
};

View file

@ -0,0 +1,26 @@
import create from 'zustand';
import { persist } from 'zustand/middleware';
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
import {
paginationSettings,
sortableSettings,
} from '@/react/components/datatables/types';
import { TableSettings } from './types';
export const TRUNCATE_LENGTH = 32;
export function createStore(storageKey: string) {
return create<TableSettings>()(
persist(
(set) => ({
...sortableSettings(set),
...paginationSettings(set),
}),
{
name: keyBuilder(storageKey),
}
)
);
}

View file

@ -0,0 +1 @@
export * from './IngressClassDatatable';

View file

@ -0,0 +1,8 @@
import {
PaginationTableSettings,
SortableTableSettings,
} from '@/react/components/datatables/types';
export interface TableSettings
extends SortableTableSettings,
PaginationTableSettings {}

View file

@ -0,0 +1,23 @@
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/portainer/environments/types';
import {
KubernetesApiListResponse,
V1IngressClass,
} from '@/react/kubernetes/services/kubernetes/types';
export async function getAllIngressClasses(environmentId: EnvironmentId) {
try {
const {
data: { items },
} = await axios.get<KubernetesApiListResponse<V1IngressClass[]>>(
urlBuilder(environmentId)
);
return items;
} catch (error) {
throw parseAxiosError(error as Error);
}
}
function urlBuilder(environmentId: EnvironmentId) {
return `endpoints/${environmentId}/kubernetes/apis/networking.k8s.io/v1/ingressclasses`;
}

View file

@ -0,0 +1,10 @@
type SupportedIngControllerNames = 'nginx' | 'traefik' | 'unknown';
export interface IngressControllerClassMap extends Record<string, unknown> {
Name: string;
ClassName: string;
Type: SupportedIngControllerNames;
Availability: boolean;
New: boolean;
Used: boolean; // if the controller is used by any ingress in the cluster
}

View file

@ -0,0 +1,57 @@
import { EnvironmentId } from '@/portainer/environments/types';
import PortainerError from '@/portainer/error';
import axios from '@/portainer/services/axios';
import { IngressControllerClassMap } from '../types';
// get all supported ingress classes and controllers for the cluster
// allowedOnly set to true will hide globally disallowed ingresscontrollers
export async function getIngressControllerClassMap({
environmentId,
namespace,
allowedOnly,
}: {
environmentId: EnvironmentId;
namespace?: string;
allowedOnly?: boolean;
}) {
try {
const { data: controllerMaps } = await axios.get<
IngressControllerClassMap[]
>(
buildUrl(environmentId, namespace),
allowedOnly ? { params: { allowedOnly: true } } : undefined
);
return controllerMaps;
} catch (e) {
throw new PortainerError('Unable to get ingress controllers.', e as Error);
}
}
// get all supported ingress classes and controllers for the cluster
export async function updateIngressControllerClassMap(
environmentId: EnvironmentId,
ingressControllerClassMap: IngressControllerClassMap[],
namespace?: string
) {
try {
const { data: controllerMaps } = await axios.put<
IngressControllerClassMap[]
>(buildUrl(environmentId, namespace), ingressControllerClassMap);
return controllerMaps;
} catch (e) {
throw new PortainerError(
'Unable to update ingress controllers.',
e as Error
);
}
}
function buildUrl(environmentId: EnvironmentId, namespace?: string) {
let url = `kubernetes/${environmentId}/`;
if (namespace) {
url += `namespaces/${namespace}/`;
}
url += 'ingresscontrollers';
return url;
}