fix(docker/networks): load containers from target node [EE-5446] (#8927)

pull/8975/head
Chaim Lev-Ari 2023-05-18 12:53:30 +07:00 committed by GitHub
parent a35e18a904
commit 8e785e8bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 216 additions and 198 deletions

View File

@ -27,6 +27,8 @@ axios.interceptors.request.use(async (config) => {
return newConfig; return newConfig;
}); });
export const agentTargetHeader = 'X-PortainerAgent-Target';
export function agentInterceptor(config: AxiosRequestConfig) { export function agentInterceptor(config: AxiosRequestConfig) {
if (!config.url || !config.url.includes('/docker/')) { if (!config.url || !config.url.includes('/docker/')) {
return config; return config;
@ -35,7 +37,7 @@ export function agentInterceptor(config: AxiosRequestConfig) {
const newConfig = { headers: config.headers || {}, ...config }; const newConfig = { headers: config.headers || {}, ...config };
const target = portainerAgentTargetHeader(); const target = portainerAgentTargetHeader();
if (target) { if (target) {
newConfig.headers['X-PortainerAgent-Target'] = target; newConfig.headers[agentTargetHeader] = target;
} }
if (portainerAgentManagerOperation()) { if (portainerAgentManagerOperation()) {

View File

@ -52,12 +52,9 @@ export function ContainersDatatable({
const [search, setSearch] = useSearchBarState(storageKey); const [search, setSearch] = useSearchBarState(storageKey);
const containersQuery = useContainers( const containersQuery = useContainers(environment.Id, {
environment.Id, autoRefreshRate: settings.autoRefreshRate * 1000,
true, });
undefined,
settings.autoRefreshRate * 1000
);
return ( return (
<RowProvider context={{ environment }}> <RowProvider context={{ environment }}>

View File

@ -1,7 +1,11 @@
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios'; import axios, {
agentTargetHeader,
parseAxiosError,
} from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
import { urlBuilder } from '../containers.service'; import { urlBuilder } from '../containers.service';
import { DockerContainerResponse } from '../types/response'; import { DockerContainerResponse } from '../types/response';
@ -10,20 +14,27 @@ import { parseViewModel } from '../utils';
import { Filters } from './types'; import { Filters } from './types';
import { queryKeys } from './query-keys'; import { queryKeys } from './query-keys';
interface UseContainers {
all?: boolean;
filters?: Filters;
nodeName?: string;
}
export function useContainers( export function useContainers(
environmentId: EnvironmentId, environmentId: EnvironmentId,
all = true, {
filters?: Filters, autoRefreshRate,
autoRefreshRate?: number
...params
}: UseContainers & {
autoRefreshRate?: number;
} = {}
) { ) {
return useQuery( return useQuery(
queryKeys.filters(environmentId, all, filters), queryKeys.filters(environmentId, params),
() => getContainers(environmentId, all, filters), () => getContainers(environmentId, params),
{ {
meta: { ...withGlobalError('Unable to retrieve containers'),
title: 'Failure',
message: 'Unable to retrieve containers',
},
refetchInterval() { refetchInterval() {
return autoRefreshRate ?? false; return autoRefreshRate ?? false;
}, },
@ -33,14 +44,18 @@ export function useContainers(
async function getContainers( async function getContainers(
environmentId: EnvironmentId, environmentId: EnvironmentId,
all = true, { all = true, filters, nodeName }: UseContainers = {}
filters?: Filters
) { ) {
try { try {
const { data } = await axios.get<DockerContainerResponse[]>( const { data } = await axios.get<DockerContainerResponse[]>(
urlBuilder(environmentId, undefined, 'json'), urlBuilder(environmentId, undefined, 'json'),
{ {
params: { all, filters: filters && JSON.stringify(filters) }, params: { all, filters: filters && JSON.stringify(filters) },
headers: nodeName
? {
[agentTargetHeader]: nodeName,
}
: undefined,
} }
); );
return data.map((c) => parseViewModel(c)); return data.map((c) => parseViewModel(c));

View File

@ -8,8 +8,10 @@ export const queryKeys = {
list: (environmentId: EnvironmentId) => list: (environmentId: EnvironmentId) =>
[dockerQueryKeys.root(environmentId), 'containers'] as const, [dockerQueryKeys.root(environmentId), 'containers'] as const,
filters: (environmentId: EnvironmentId, all?: boolean, filters?: Filters) => filters: (
[...queryKeys.list(environmentId), { all, filters }] as const, environmentId: EnvironmentId,
params: { all?: boolean; filters?: Filters; nodeName?: string } = {}
) => [...queryKeys.list(environmentId), params] as const,
container: (environmentId: EnvironmentId, id: string) => container: (environmentId: EnvironmentId, id: string) =>
[...queryKeys.list(environmentId), id] as const, [...queryKeys.list(environmentId), id] as const,

View File

@ -1,7 +1,5 @@
import { useState, useEffect } from 'react';
import { useRouter, useCurrentStateAndParams } from '@uirouter/react'; import { useRouter, useCurrentStateAndParams } from '@uirouter/react';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import _ from 'lodash';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel/AccessControlPanel'; import { AccessControlPanel } from '@/react/portainer/access-control/AccessControlPanel/AccessControlPanel';
@ -15,7 +13,7 @@ import { PageHeader } from '@@/PageHeader';
import { useNetwork, useDeleteNetwork } from '../queries'; import { useNetwork, useDeleteNetwork } from '../queries';
import { isSystemNetwork } from '../network.helper'; import { isSystemNetwork } from '../network.helper';
import { DockerNetwork, NetworkContainer } from '../types'; import { NetworkResponseContainers } from '../types';
import { NetworkDetailsTable } from './NetworkDetailsTable'; import { NetworkDetailsTable } from './NetworkDetailsTable';
import { NetworkOptionsTable } from './NetworkOptionsTable'; import { NetworkOptionsTable } from './NetworkOptionsTable';
@ -25,28 +23,18 @@ export function ItemView() {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [networkContainers, setNetworkContainers] = useState<
NetworkContainer[]
>([]);
const { const {
params: { id: networkId, nodeName }, params: { id: networkId, nodeName },
} = useCurrentStateAndParams(); } = useCurrentStateAndParams();
const environmentId = useEnvironmentId(); const environmentId = useEnvironmentId();
const networkQuery = useNetwork(environmentId, networkId, { nodeName });
const networkQuery = useNetwork(environmentId, networkId);
const deleteNetworkMutation = useDeleteNetwork(); const deleteNetworkMutation = useDeleteNetwork();
const filters = { const containersQuery = useContainers(environmentId, {
filters: {
network: [networkId], network: [networkId],
}; },
const containersQuery = useContainers(environmentId, true, filters); nodeName,
});
useEffect(() => {
if (networkQuery.data && containersQuery.data) {
setNetworkContainers(
filterContainersInNetwork(networkQuery.data, containersQuery.data)
);
}
}, [networkQuery.data, containersQuery.data]);
if (!networkQuery.data) { if (!networkQuery.data) {
return null; return null;
@ -54,6 +42,10 @@ export function ItemView() {
const network = networkQuery.data; const network = networkQuery.data;
const networkContainers = filterContainersInNetwork(
network.Containers,
containersQuery.data
);
const resourceControl = network.Portainer?.ResourceControl const resourceControl = network.Portainer?.ResourceControl
? new ResourceControlViewModel(network.Portainer.ResourceControl) ? new ResourceControlViewModel(network.Portainer.ResourceControl)
: undefined; : undefined;
@ -116,24 +108,20 @@ export function ItemView() {
); );
} }
} }
}
function filterContainersInNetwork( function filterContainersInNetwork(
network: DockerNetwork, networkContainers?: NetworkResponseContainers,
containers: DockerContainer[] containers: DockerContainer[] = []
) { ) {
const containersInNetwork = _.compact( if (!networkContainers) {
containers.map((container) => { return [];
const containerInNetworkResponse = network.Containers[container.Id]; }
if (containerInNetworkResponse) {
const containerInNetwork: NetworkContainer = { return containers
...containerInNetworkResponse, .filter((container) => networkContainers[container.Id])
.map((container) => ({
...networkContainers[container.Id],
Id: container.Id, Id: container.Id,
}; }));
return containerInNetwork;
}
return null;
})
);
return containersInNetwork;
}
} }

View File

@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { Icon } from '@/react/components/Icon'; import { Icon } from '@/react/components/Icon';
import { Table, TableContainer, TableTitle } from '@@/datatables'; import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable'; import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
@ -42,7 +42,6 @@ export function NetworkContainersTable({
return ( return (
<TableContainer> <TableContainer>
<TableTitle label="Containers in network" icon={Server} /> <TableTitle label="Containers in network" icon={Server} />
<Table className="nopadding">
<DetailsTable <DetailsTable
headers={tableHeaders} headers={tableHeaders}
dataCy="networkDetails-networkContainers" dataCy="networkDetails-networkContainers"
@ -88,7 +87,6 @@ export function NetworkContainersTable({
</tr> </tr>
))} ))}
</DetailsTable> </DetailsTable>
</Table>
</TableContainer> </TableContainer>
); );
} }

View File

@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
import DockerNetworkHelper from '@/docker/helpers/networkHelper'; import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { Authorized } from '@/react/hooks/useUser'; import { Authorized } from '@/react/hooks/useUser';
import { Table, TableContainer, TableTitle } from '@@/datatables'; import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable'; import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { Icon } from '@@/Icon'; import { Icon } from '@@/Icon';
@ -32,7 +32,6 @@ export function NetworkDetailsTable({
return ( return (
<TableContainer> <TableContainer>
<TableTitle label="Network details" icon={Share2} /> <TableTitle label="Network details" icon={Share2} />
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-detailsTable"> <DetailsTable dataCy="networkDetails-detailsTable">
{/* networkRowContent */} {/* networkRowContent */}
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row> <DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
@ -101,7 +100,6 @@ export function NetworkDetailsTable({
</Fragment> </Fragment>
))} ))}
</DetailsTable> </DetailsTable>
</Table>
</TableContainer> </TableContainer>
); );

View File

@ -1,6 +1,6 @@
import { Share2 } from 'lucide-react'; import { Share2 } from 'lucide-react';
import { Table, TableContainer, TableTitle } from '@@/datatables'; import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable'; import { DetailsTable } from '@@/DetailsTable';
import { NetworkOptions } from '../types'; import { NetworkOptions } from '../types';
@ -19,7 +19,6 @@ export function NetworkOptionsTable({ options }: Props) {
return ( return (
<TableContainer> <TableContainer>
<TableTitle label="Network options" icon={Share2} /> <TableTitle label="Network options" icon={Share2} />
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-networkOptionsTable"> <DetailsTable dataCy="networkDetails-networkOptionsTable">
{networkEntries.map(([key, value]) => ( {networkEntries.map(([key, value]) => (
<DetailsTable.Row key={key} label={key}> <DetailsTable.Row key={key} label={key}>
@ -27,7 +26,6 @@ export function NetworkOptionsTable({ options }: Props) {
</DetailsTable.Row> </DetailsTable.Row>
))} ))}
</DetailsTable> </DetailsTable>
</Table>
</TableContainer> </TableContainer>
); );
} }

View File

@ -1,5 +1,8 @@
import { ContainerId } from '@/react/docker/containers/types'; import { ContainerId } from '@/react/docker/containers/types';
import axios, { parseAxiosError } from '@/portainer/services/axios'; import axios, {
agentTargetHeader,
parseAxiosError,
} from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { NetworkId, DockerNetwork } from './types'; import { NetworkId, DockerNetwork } from './types';
@ -8,11 +11,19 @@ type NetworkAction = 'connect' | 'disconnect' | 'create';
export async function getNetwork( export async function getNetwork(
environmentId: EnvironmentId, environmentId: EnvironmentId,
networkId: NetworkId networkId: NetworkId,
{ nodeName }: { nodeName?: string } = {}
) { ) {
try { try {
const { data: network } = await axios.get<DockerNetwork>( const { data: network } = await axios.get<DockerNetwork>(
buildUrl(environmentId, networkId) buildUrl(environmentId, networkId),
nodeName
? {
headers: {
[agentTargetHeader]: nodeName,
},
}
: undefined
); );
return network; return network;
} catch (e) { } catch (e) {

View File

@ -14,10 +14,21 @@ import {
} from './network.service'; } from './network.service';
import { NetworkId } from './types'; import { NetworkId } from './types';
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) { export function useNetwork(
environmentId: EnvironmentId,
networkId: NetworkId,
{ nodeName }: { nodeName?: string } = {}
) {
return useQuery( return useQuery(
['environments', environmentId, 'docker', 'networks', networkId], [
() => getNetwork(environmentId, networkId), 'environments',
environmentId,
'docker',
'networks',
networkId,
{ nodeName },
],
() => getNetwork(environmentId, networkId, { nodeName }),
{ {
onError: (err) => { onError: (err) => {
notifyError('Failure', err as Error, 'Unable to get network'); notifyError('Failure', err as Error, 'Unable to get network');

View File

@ -49,14 +49,12 @@ export function StackContainersDatatable({ environment, stackName }: Props) {
columns.filter((col) => col.canHide).map((col) => col.id) columns.filter((col) => col.canHide).map((col) => col.id)
); );
const containersQuery = useContainers( const containersQuery = useContainers(environment.Id, {
environment.Id, filters: {
true,
{
label: [`com.docker.compose.project=${stackName}`], label: [`com.docker.compose.project=${stackName}`],
}, },
settings.autoRefreshRate * 1000 autoRefreshRate: settings.autoRefreshRate * 1000,
); });
return ( return (
<RowProvider context={{ environment }}> <RowProvider context={{ environment }}>