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

pull/8967/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;
});
export const agentTargetHeader = 'X-PortainerAgent-Target';
export function agentInterceptor(config: AxiosRequestConfig) {
if (!config.url || !config.url.includes('/docker/')) {
return config;
@ -35,7 +37,7 @@ export function agentInterceptor(config: AxiosRequestConfig) {
const newConfig = { headers: config.headers || {}, ...config };
const target = portainerAgentTargetHeader();
if (target) {
newConfig.headers['X-PortainerAgent-Target'] = target;
newConfig.headers[agentTargetHeader] = target;
}
if (portainerAgentManagerOperation()) {

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Icon } from '@/react/components/Icon';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
@ -42,53 +42,51 @@ export function NetworkContainersTable({
return (
<TableContainer>
<TableTitle label="Containers in network" icon={Server} />
<Table className="nopadding">
<DetailsTable
headers={tableHeaders}
dataCy="networkDetails-networkContainers"
>
{networkContainers.map((container) => (
<tr key={container.Id}>
<td>
<Link
to="docker.containers.container"
params={{
id: container.Id,
nodeName,
<DetailsTable
headers={tableHeaders}
dataCy="networkDetails-networkContainers"
>
{networkContainers.map((container) => (
<tr key={container.Id}>
<td>
<Link
to="docker.containers.container"
params={{
id: container.Id,
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}
</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,
});
}
}}
>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</DetailsTable>
</Table>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</DetailsTable>
</TableContainer>
);
}

View File

@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { Authorized } from '@/react/hooks/useUser';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
@ -32,76 +32,74 @@ export function NetworkDetailsTable({
return (
<TableContainer>
<TableTitle label="Network details" icon={Share2} />
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-detailsTable">
{/* networkRowContent */}
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
<DetailsTable.Row label="Id">
{network.Id}
{allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete">
<Button
data-cy="networkDetails-deleteNetwork"
size="xsmall"
color="danger"
onClick={() => onRemoveNetworkClicked()}
>
<Icon
icon={Trash2}
className="space-right"
aria-hidden="true"
/>
Delete this network
</Button>
</Authorized>
)}
</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
<DetailsTable.Row label="Attachable">
{String(network.Attachable)}
</DetailsTable.Row>
<DetailsTable.Row label="Internal">
{String(network.Internal)}
</DetailsTable.Row>
<DetailsTable dataCy="networkDetails-detailsTable">
{/* networkRowContent */}
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
<DetailsTable.Row label="Id">
{network.Id}
{allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete">
<Button
data-cy="networkDetails-deleteNetwork"
size="xsmall"
color="danger"
onClick={() => onRemoveNetworkClicked()}
>
<Icon
icon={Trash2}
className="space-right"
aria-hidden="true"
/>
Delete this network
</Button>
</Authorized>
)}
</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
<DetailsTable.Row label="Attachable">
{String(network.Attachable)}
</DetailsTable.Row>
<DetailsTable.Row label="Internal">
{String(network.Internal)}
</DetailsTable.Row>
{/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
{/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
{/* IPV6 ConfigRowContent */}
{ipv6Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
</DetailsTable>
</Table>
{/* IPV6 ConfigRowContent */}
{ipv6Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
</DetailsTable>
</TableContainer>
);

View File

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

View File

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

View File

@ -14,10 +14,21 @@ import {
} from './network.service';
import { NetworkId } from './types';
export function useNetwork(environmentId: EnvironmentId, networkId: NetworkId) {
export function useNetwork(
environmentId: EnvironmentId,
networkId: NetworkId,
{ nodeName }: { nodeName?: string } = {}
) {
return useQuery(
['environments', environmentId, 'docker', 'networks', networkId],
() => getNetwork(environmentId, networkId),
[
'environments',
environmentId,
'docker',
'networks',
networkId,
{ nodeName },
],
() => getNetwork(environmentId, networkId, { nodeName }),
{
onError: (err) => {
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)
);
const containersQuery = useContainers(
environment.Id,
true,
{
const containersQuery = useContainers(environment.Id, {
filters: {
label: [`com.docker.compose.project=${stackName}`],
},
settings.autoRefreshRate * 1000
);
autoRefreshRate: settings.autoRefreshRate * 1000,
});
return (
<RowProvider context={{ environment }}>