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, {
network: [networkId], filters: {
}; 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(
network: DockerNetwork, function filterContainersInNetwork(
containers: DockerContainer[] networkContainers?: NetworkResponseContainers,
) { containers: DockerContainer[] = []
const containersInNetwork = _.compact( ) {
containers.map((container) => { if (!networkContainers) {
const containerInNetworkResponse = network.Containers[container.Id]; return [];
if (containerInNetworkResponse) { }
const containerInNetwork: NetworkContainer = {
...containerInNetworkResponse, return containers
Id: container.Id, .filter((container) => networkContainers[container.Id])
}; .map((container) => ({
return containerInNetwork; ...networkContainers[container.Id],
} Id: container.Id,
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,53 +42,51 @@ 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" >
> {networkContainers.map((container) => (
{networkContainers.map((container) => ( <tr key={container.Id}>
<tr key={container.Id}> <td>
<td> <Link
<Link to="docker.containers.container"
to="docker.containers.container" params={{
params={{ id: container.Id,
id: container.Id, nodeName,
nodeName, }}
title={container.Name}
>
{container.Name}
</Link>
</td>
<td>{container.IPv4Address || '-'}</td>
<td>{container.IPv6Address || '-'}</td>
<td>{container.MacAddress || '-'}</td>
<td>
<Authorized authorizations="DockerNetworkDisconnect">
<Button
data-cy={`networkDetails-disconnect${container.Name}`}
size="xsmall"
color="dangerlight"
onClick={() => {
if (container.Id) {
disconnectContainer.mutate({
containerId: container.Id,
environmentId,
networkId,
});
}
}} }}
title={container.Name}
> >
{container.Name} <Icon icon={Trash2} class-name="icon-secondary icon-md" />
</Link> Leave Network
</td> </Button>
<td>{container.IPv4Address || '-'}</td> </Authorized>
<td>{container.IPv6Address || '-'}</td> </td>
<td>{container.MacAddress || '-'}</td> </tr>
<td> ))}
<Authorized authorizations="DockerNetworkDisconnect"> </DetailsTable>
<Button
data-cy={`networkDetails-disconnect${container.Name}`}
size="xsmall"
color="dangerlight"
onClick={() => {
if (container.Id) {
disconnectContainer.mutate({
containerId: container.Id,
environmentId,
networkId,
});
}
}}
>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</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,76 +32,74 @@ 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> <DetailsTable.Row label="Id">
<DetailsTable.Row label="Id"> {network.Id}
{network.Id} {allowRemoveNetwork && (
{allowRemoveNetwork && ( <Authorized authorizations="DockerNetworkDelete">
<Authorized authorizations="DockerNetworkDelete"> <Button
<Button data-cy="networkDetails-deleteNetwork"
data-cy="networkDetails-deleteNetwork" size="xsmall"
size="xsmall" color="danger"
color="danger" onClick={() => onRemoveNetworkClicked()}
onClick={() => onRemoveNetworkClicked()} >
> <Icon
<Icon icon={Trash2}
icon={Trash2} className="space-right"
className="space-right" aria-hidden="true"
aria-hidden="true" />
/> Delete this network
Delete this network </Button>
</Button> </Authorized>
</Authorized> )}
)} </DetailsTable.Row>
</DetailsTable.Row> <DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row> <DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row> <DetailsTable.Row label="Attachable">
<DetailsTable.Row label="Attachable"> {String(network.Attachable)}
{String(network.Attachable)} </DetailsTable.Row>
</DetailsTable.Row> <DetailsTable.Row label="Internal">
<DetailsTable.Row label="Internal"> {String(network.Internal)}
{String(network.Internal)} </DetailsTable.Row>
</DetailsTable.Row>
{/* IPV4 ConfigRowContent */} {/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => ( {ipv4Configs.map((config) => (
<Fragment key={config.Subnet}> <Fragment key={config.Subnet}>
<DetailsTable.Row <DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`} label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
> >
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`} {`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row> </DetailsTable.Row>
<DetailsTable.Row <DetailsTable.Row
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`} label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
> >
{`IPV4 Excluded IPs${getAuxiliaryAddresses( {`IPV4 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses config.AuxiliaryAddresses
)}`} )}`}
</DetailsTable.Row> </DetailsTable.Row>
</Fragment> </Fragment>
))} ))}
{/* IPV6 ConfigRowContent */} {/* IPV6 ConfigRowContent */}
{ipv6Configs.map((config) => ( {ipv6Configs.map((config) => (
<Fragment key={config.Subnet}> <Fragment key={config.Subnet}>
<DetailsTable.Row <DetailsTable.Row
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`} label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
> >
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`} {`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row> </DetailsTable.Row>
<DetailsTable.Row <DetailsTable.Row
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`} label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
> >
{`IPV6 Excluded IPs${getAuxiliaryAddresses( {`IPV6 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses config.AuxiliaryAddresses
)}`} )}`}
</DetailsTable.Row> </DetailsTable.Row>
</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,15 +19,13 @@ 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}> {value}
{value} </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 }}>