1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

refactor(k8s): namespace core logic (#12142)

Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
This commit is contained in:
Steven Kang 2024-10-01 14:15:51 +13:00 committed by GitHub
parent da010f3d08
commit ea228c3d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
276 changed files with 9241 additions and 3361 deletions

View file

@ -71,10 +71,7 @@ export function CreateIngressView() {
const { data: allServices } = useNamespaceServices(environmentId, namespace);
const secretsResults = useSecrets(environmentId, namespace);
const ingressesResults = useIngresses(
environmentId,
namespaces ? Object.keys(namespaces || {}) : []
);
const ingressesResults = useIngresses(environmentId);
const { data: ingressControllers, ...ingressControllersQuery } =
useIngressControllers(environmentId, namespace);
@ -90,7 +87,7 @@ export function CreateIngressView() {
] => {
const ruleCounterByNamespace: Record<string, number> = {};
const hostWithTLS: Record<string, string> = {};
ingressesResults.data?.forEach((ingress) => {
ingressesResults.data?.forEach((ingress: Ingress) => {
ingress.TLS?.forEach((tls) => {
tls.Hosts.forEach((host) => {
hostWithTLS[host] = tls.SecretName;
@ -98,7 +95,7 @@ export function CreateIngressView() {
});
});
const ingressNames: string[] = [];
ingressesResults.data?.forEach((ing) => {
ingressesResults.data?.forEach((ing: Ingress) => {
ruleCounterByNamespace[ing.Namespace] =
ruleCounterByNamespace[ing.Namespace] || 0;
const n = ing.Name.match(/^(.*)-(\d+)$/);
@ -123,10 +120,10 @@ export function CreateIngressView() {
const namespaceOptions = useMemo(
() =>
Object.entries(namespaces || {})
.filter(([, nsValue]) => !nsValue.IsSystem)
.map(([nsKey]) => ({
label: nsKey,
value: nsKey,
.filter(([, ns]) => !ns.IsSystem)
.map(([, ns]) => ({
label: ns.Name,
value: ns.Name,
})),
[namespaces]
);
@ -170,10 +167,10 @@ export function CreateIngressView() {
? Object.fromEntries(
allServices?.map((service) => [
service.Name,
service.Ports.map((port) => ({
service.Ports?.map((port) => ({
label: String(port.Port),
value: String(port.Port),
})),
})) ?? [],
])
)
: {},

View file

@ -4,27 +4,20 @@ import { useMemo } from 'react';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useAuthorizations, Authorized } from '@/react/hooks/useUser';
import Route from '@/assets/ico/route.svg?c';
import {
DefaultDatatableSettings,
TableSettings as KubeTableSettings,
} from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { useKubeStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { useIsDeploymentOptionHidden } from '@/react/hooks/useIsDeploymentOptionHidden';
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import { AddButton } from '@@/buttons';
import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import {
type FilteredColumnsTableSettings,
filteredColumnsSettings,
} from '@@/datatables/types';
import { mergeOptions } from '@@/datatables/extend-options/mergeOptions';
import { withColumnFilters } from '@@/datatables/extend-options/withColumnFilters';
import { DeleteIngressesRequest, Ingress } from '../types';
import { useDeleteIngresses, useIngresses } from '../queries';
import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery';
import { Namespaces } from '../../namespaces/types';
import { Namespaces, PortainerNamespace } from '../../namespaces/types';
import { CreateFromManifestButton } from '../../components/CreateFromManifestButton';
import { columns } from './columns';
@ -37,48 +30,48 @@ interface SelectedIngress {
}
const storageKey = 'ingressClassesNameSpace';
interface TableSettings
extends KubeTableSettings,
FilteredColumnsTableSettings {}
const settingsStore = createStore(storageKey, 'name');
export function IngressDatatable() {
const tableState = useKubeStore<TableSettings>(
storageKey,
undefined,
(set) => ({
...filteredColumnsSettings(set),
})
);
const tableState = useTableState(settingsStore, storageKey);
const environmentId = useEnvironmentId();
const { authorized: canAccessSystemResources } = useAuthorizations(
'K8sAccessSystemNamespaces'
);
const { data: namespaces, ...namespacesQuery } =
useNamespacesQuery(environmentId);
const { data: ingresses, ...ingressesQuery } = useIngresses(
environmentId,
Object.keys(namespaces || {}),
{
autoRefreshRate: tableState.autoRefreshRate * 1000,
}
);
const namespacesQuery = useNamespacesQuery(environmentId);
const { data: ingresses, ...ingressesQuery } = useIngresses(environmentId, {
autoRefreshRate: tableState.autoRefreshRate * 1000,
withServices: true,
});
const namespacesMap = useMemo(() => {
const namespacesMap = namespacesQuery.data?.reduce<
Record<string, PortainerNamespace>
>((acc, namespace) => {
acc[namespace.Name] = namespace;
return acc;
}, {});
return namespacesMap ?? {};
}, [namespacesQuery.data]);
const filteredIngresses = useMemo(
() =>
ingresses?.filter(
(ingress) =>
(canAccessSystemResources && tableState.showSystemResources) ||
!namespaces?.[ingress.Namespace].IsSystem
!namespacesMap?.[ingress.Namespace].IsSystem
) || [],
[ingresses, tableState, canAccessSystemResources, namespaces]
[ingresses, tableState, canAccessSystemResources, namespacesMap]
);
const ingressesWithIsSystem = useIngressesRowData(
filteredIngresses || [],
namespaces
namespacesMap
);
const isAddIngressHidden = useIsDeploymentOptionHidden('form');
const deleteIngressesMutation = useDeleteIngresses();
const router = useRouter();
@ -89,10 +82,13 @@ export function IngressDatatable() {
dataset={ingressesWithIsSystem}
columns={columns}
isLoading={ingressesQuery.isLoading || namespacesQuery.isLoading}
emptyContentLabel="No supported ingresses found"
title="Ingresses"
titleIcon={Route}
getRowId={(row) => row.Name + row.Type + row.Namespace}
isRowSelectable={(row) => !namespaces?.[row.original.Namespace].IsSystem}
isRowSelectable={(row) =>
!namespacesMap?.[row.original.Namespace].IsSystem
}
renderTableActions={tableActions}
renderTableSettings={() => (
<TableSettingsMenu>
@ -106,9 +102,6 @@ export function IngressDatatable() {
}
disableSelect={useCheckboxes()}
data-cy="k8s-ingresses-datatable"
extendTableOptions={mergeOptions(
withColumnFilters(tableState.columnFilters, tableState.setColumnFilters)
)}
/>
);
@ -137,9 +130,15 @@ export function IngressDatatable() {
data-cy="remove-ingresses-button"
/>
<AddButton to=".create" color="secondary" data-cy="add-ingress-button">
Add with form
</AddButton>
{!isAddIngressHidden && (
<AddButton
to=".create"
color="secondary"
data-cy="add-ingress-button"
>
Add with form
</AddButton>
)}
<CreateFromManifestButton data-cy="k8s-ingress-deploy-button" />
</Authorized>

View file

@ -3,11 +3,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
mutationOptions,
withError,
withGlobalError,
withInvalidate,
} from '@/react-tools/react-query';
import { getServices } from '@/react/kubernetes/networks/services/service';
import { isFulfilled } from '@/portainer/helpers/promise-utils';
import {
getIngresses,
@ -19,13 +17,23 @@ import {
} from './service';
import { DeleteIngressesRequest, Ingress } from './types';
const ingressKeys = {
all: ['environments', 'kubernetes', 'namespace', 'ingress'] as const,
namespace: (
const queryKeys = {
base: ['environments', 'kubernetes', 'ingress'] as const,
clusterIngresses: (environmentId: EnvironmentId) =>
[...queryKeys.base, String(environmentId)] as const,
namespaceIngresses: (
environmentId: EnvironmentId,
namespace: string,
ingress: string
) => [...ingressKeys.all, String(environmentId), namespace, ingress] as const,
) => [...queryKeys.base, String(environmentId), namespace, ingress] as const,
ingress: (environmentId: EnvironmentId, namespace: string, name: string) =>
[...queryKeys.base, String(environmentId), namespace, name] as const,
ingressControllers: (environmentId: EnvironmentId, namespace: string) => [
...queryKeys.base,
String(environmentId),
namespace,
'ingresscontrollers',
],
};
export function useIngress(
@ -34,93 +42,34 @@ export function useIngress(
name: string
) {
return useQuery(
[
'environments',
environmentId,
'kubernetes',
'namespace',
namespace,
'ingress',
name,
],
queryKeys.ingress(environmentId, namespace, name),
async () => {
const ing = await getIngress(environmentId, namespace, name);
return ing;
},
{
...withError('Unable to get ingress'),
...withGlobalError('Unable to get ingress'),
}
);
}
export function useIngresses(
environmentId: EnvironmentId,
namespaces?: string[],
options?: { autoRefreshRate?: number }
options?: {
autoRefreshRate?: number;
enabled?: boolean;
withServices?: boolean;
}
) {
const { enabled, autoRefreshRate, ...params } = options ?? {};
return useQuery(
[
'environments',
environmentId,
'kubernetes',
'namespace',
namespaces,
'ingress',
],
async () => {
if (!namespaces?.length) {
return [];
}
const settledIngressesPromise = await Promise.allSettled(
namespaces.map((namespace) => getIngresses(environmentId, namespace))
);
const ingresses = settledIngressesPromise
.filter(isFulfilled)
?.map((i) => i.value);
// flatten the array and remove empty ingresses
const filteredIngresses = ingresses.flat().filter((ing) => ing);
// get all services in only the namespaces that the ingresses are in to find missing services
const uniqueNamespacesWithIngress = [
...new Set(filteredIngresses.map((ing) => ing?.Namespace)),
];
const settledServicesPromise = await Promise.allSettled(
uniqueNamespacesWithIngress.map((ns) => getServices(environmentId, ns))
);
const services = settledServicesPromise
.filter(isFulfilled)
?.map((s) => s.value)
.flat();
// check if each ingress path service has a service that still exists
const updatedFilteredIngresses: Ingress[] = filteredIngresses.map(
(ing) => {
const servicesInNamespace = services?.filter(
(service) => service?.Namespace === ing?.Namespace
);
const serviceNamesInNamespace = servicesInNamespace?.map(
(service) => service.Name
);
const updatedPaths =
ing.Paths?.map((path) => {
const hasService = serviceNamesInNamespace?.includes(
path.ServiceName
);
return { ...path, HasService: hasService };
}) || null;
return { ...ing, Paths: updatedPaths };
}
);
return updatedFilteredIngresses;
},
['environments', environmentId, 'kubernetes', 'ingress', params],
async () => getIngresses(environmentId, params),
{
enabled: !!namespaces?.length,
...withError('Unable to get ingresses'),
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
...withGlobalError('Unable to get ingresses'),
refetchInterval: autoRefreshRate,
enabled,
}
);
}
@ -136,8 +85,8 @@ export function useCreateIngress() {
ingress: Ingress;
}) => createIngress(environmentId, ingress),
mutationOptions(
withError('Unable to create ingress controller'),
withInvalidate(queryClient, [ingressKeys.all])
withGlobalError('Unable to create ingress controller'),
withInvalidate(queryClient, [queryKeys.base])
)
);
}
@ -153,8 +102,8 @@ export function useUpdateIngress() {
ingress: Ingress;
}) => updateIngress(environmentId, ingress),
mutationOptions(
withError('Unable to update ingress controller'),
withInvalidate(queryClient, [ingressKeys.all])
withGlobalError('Unable to update ingress controller'),
withInvalidate(queryClient, [queryKeys.base])
)
);
}
@ -170,8 +119,8 @@ export function useDeleteIngresses() {
data: DeleteIngressesRequest;
}) => deleteIngresses(environmentId, data),
mutationOptions(
withError('Unable to update ingress controller'),
withInvalidate(queryClient, [ingressKeys.all])
withGlobalError('Unable to update ingress controller'),
withInvalidate(queryClient, [queryKeys.base])
)
);
}
@ -185,21 +134,14 @@ export function useIngressControllers(
allowedOnly?: boolean
) {
return useQuery(
[
'environments',
environmentId,
'kubernetes',
'namespace',
namespace,
'ingresscontrollers',
],
queryKeys.ingressControllers(environmentId, namespace ?? ''),
async () =>
namespace
? getIngressControllers(environmentId, namespace, allowedOnly)
: [],
{
enabled: !!namespace,
...withError('Unable to get ingress controllers'),
...withGlobalError('Unable to get ingress controllers'),
}
);
}

View file

@ -20,11 +20,12 @@ export async function getIngress(
export async function getIngresses(
environmentId: EnvironmentId,
namespace: string
params?: { withServices?: boolean }
) {
try {
const { data: ingresses } = await axios.get<Ingress[]>(
buildUrl(environmentId, namespace)
`kubernetes/${environmentId}/ingresses`,
{ params }
);
return ingresses;
} catch (e) {