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:
parent
da010f3d08
commit
ea228c3d6d
276 changed files with 9241 additions and 3361 deletions
|
@ -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),
|
||||
})),
|
||||
})) ?? [],
|
||||
])
|
||||
)
|
||||
: {},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue