Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InventoryService } from 'app/percona/inventory/Inventory.service';
import {
nodesMockMultipleAgentsNoPMMServer,
nodesMock,
nodesMockHA,
nodesMockOneAgentNoPMMServer,
} from 'app/percona/inventory/__mocks__/Inventory.service';
import * as NodesReducer from 'app/percona/shared/core/reducers/nodes/nodes';
Expand Down Expand Up @@ -55,6 +56,29 @@ describe('Nodes Agents:: ', () => {
await waitFor(() => expect(screen.getByTestId('node')).toHaveTextContent(nodesMock[0].node_id));
});

it('should not offer nodes internal to the PMM deployment', async () => {
jest.spyOn(InventoryService, 'getNodes').mockReturnValue(Promise.resolve({ nodes: nodesMockHA }));

setup();

await waitFor(() => expect(fetchNodesActionActionSpy).toHaveBeenCalled());

selectEvent.openMenu(screen.getByLabelText('Nodes'));

expect(screen.queryByText('pmm-pmm-ha-pg-db-instance1-qjjl-0')).not.toBeInTheDocument();
expect(screen.getByText('pmm-ha-0')).toBeInTheDocument();
});

it('should prefer a node other than a PMM Server one in an HA deployment', async () => {
jest.spyOn(InventoryService, 'getNodes').mockReturnValue(Promise.resolve({ nodes: nodesMockHA }));

setup();

await waitFor(() => expect(fetchNodesActionActionSpy).toHaveBeenCalled());

await waitFor(() => expect(screen.getByTestId('node')).toHaveTextContent('external-client-id'));
});

it('should not pick any agent when the selected node is not pmm-server', async () => {
jest
.spyOn(InventoryService, 'getNodes')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,17 @@ export const NodesAgents: FC<NodesAgentsProps> = ({ form }) => {
if (nodesOptions.length === 0) {
loadData();
} else if (!selectedNode) {
// preselect pmm-server node
const pmmServerNode =
nodesOptions.find((node) => node.value === PMM_SERVER_NODE_ID) ||
// A deployment with more than one PMM Server node runs in HA mode, where the PMM Server
// pods are meant to stay free of monitoring workloads. Prefer any other node there,
// otherwise preselect the pmm-server node as usual.
const isHighlyAvailable = nodesOptions.filter((node) => node.isPMMServerNode).length > 1;
const preselectedNode =
(isHighlyAvailable ? nodesOptions.find((node) => !node.isPMMServerNode) : undefined) ??
nodesOptions.find((node) => node.value === PMM_SERVER_NODE_ID) ??
nodesOptions.find((node) => node.isPMMServerNode);

if (pmmServerNode) {
setNodeAndAgent(pmmServerNode);
if (preselectedNode) {
setNodeAndAgent(preselectedNode);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
1 change: 1 addition & 0 deletions public/app/percona/inventory/Inventory.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export interface NodeDB {
status: ServiceStatus;
services?: ServiceNodeListDB[];
is_pmm_server_node: boolean;
is_pmm_internal_node?: boolean;
}

export interface NodeListDBPayload {
Expand Down
39 changes: 39 additions & 0 deletions public/app/percona/inventory/__mocks__/Inventory.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ export const nodesMock = [
},
];

const haNodeMock = (nodeId: string, nodeName: string, isPMMServerNode: boolean, isInternalNode = false) => ({
node_id: nodeId,
node_type: 'generic',
node_name: nodeName,
is_pmm_server_node: isPMMServerNode,
is_pmm_internal_node: isInternalNode,
machine_id: '',
distro: '',
node_model: '',
container_id: '',
container_name: '',
address: '10.1.2.3',
region: '',
az: '',
custom_labels: {},
created_at: '2026-07-27T08:05:31.079300Z',
updated_at: '2026-07-27T08:05:31.079300Z',
status: ServiceStatus.UP,
agents: [
{
agent_id: `${nodeId}-pmm-agent`,
agent_type: AgentType.pmmAgent,
status: ServiceAgentStatus.RUNNING,
is_connected: true,
},
],
services: [],
});

// Mimics a PMM HA deployment: three PMM Server Nodes, an external PMM Client and the Nodes of
// PMM's own PostgreSQL cluster, which PMM Server reports as internal.
export const nodesMockHA = [
haNodeMock('pmm-ha-0-id', 'pmm-ha-0', true),
haNodeMock('pmm-ha-1-id', 'pmm-ha-1', true),
haNodeMock('pmm-ha-2-id', 'pmm-ha-2', true),
haNodeMock('external-client-id', 'external-client', false),
haNodeMock('pg-db-instance1-id', 'pmm-pmm-ha-pg-db-instance1-qjjl-0', false, true),
];

export const nodesMockMultipleAgentsNoPMMServer = [
{
node_id: '324234234',
Expand Down
3 changes: 3 additions & 0 deletions public/app/percona/shared/core/reducers/nodes/nodes.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export const nodeFromDbMapper = (nodeFromDb: NodeDB[]): Node[] => {

export const nodesOptionsMapper = (nodeFromDb: NodeDB[]): NodesOption[] =>
nodeFromDb
// Nodes belonging to a PMM deployment's own infrastructure (e.g. the HA persistence layer)
// are dedicated and must not be delegated any monitoring workloads.
.filter((node) => !node.is_pmm_internal_node)
.map((node) => {
const agents = (node.agents || [])
.filter((agent) => agent.agent_type === AgentType.pmmAgent)
Expand Down
Loading