Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -70,9 +70,11 @@ describe('AddRemoteInstanceService:: ', () => {
pmm_agent_id: {
value: 'pmm-server',
},
// the exporter runs on the PMM Server node, which is what makes the metrics mode PULL
node: {
value: 'node1',
label: 'node1',
isPMMServerNode: true,
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CancelToken } from 'axios';

import { PMM_SERVER_NODE_AGENT_ID } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.constants';
import { MetricsMode } from 'app/percona/inventory/Inventory.types';
import { Databases } from 'app/percona/shared/core';
import { apiManagement } from 'app/percona/shared/helpers/api';
Expand Down Expand Up @@ -207,7 +206,7 @@ export const toPayload = (values: any, discoverName?: string, type?: InstanceAva

data.pmm_agent_id = values.pmm_agent_id.value;

if (data.pmm_agent_id === PMM_SERVER_NODE_AGENT_ID || data.node.isPMMServerNode) {
if (data.node.isPMMServerNode) {
data.metrics_mode = MetricsMode.PULL;
} else {
data.metrics_mode = MetricsMode.PUSH;
Expand Down

This file was deleted.

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,33 @@ 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'));

// the PostgreSQL cluster backing PMM and the PMM Server nodes themselves
expect(screen.queryByText('pmm-pmm-ha-pg-db-instance1-qjjl-0')).not.toBeInTheDocument();
expect(screen.queryByText('pmm-ha-0')).not.toBeInTheDocument();
// the client is both the selected value and an option, which also proves the menu is open and
// the assertions above are not passing vacuously
expect(screen.getAllByText('pmm-pmm-ha-client-0').length).toBeGreaterThan(1);
});

it('should preselect the pre-provisioned client 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('pmm-ha-client-0-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 @@ -4,13 +4,9 @@ import { useField } from 'react-final-form';
import { useStyles2 } from '@grafana/ui';
import { Messages } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.messages';
import { getStyles } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.styles';
import {
PMM_SERVER_NODE_AGENT_ID,
PMM_SERVER_NODE_ID,
} from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.constants';
import { NodesAgentsProps } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.types';
import { GET_NODES_CANCEL_TOKEN } from 'app/percona/inventory/Inventory.constants';
import { AgentsOption, NodesOption } from 'app/percona/inventory/Inventory.types';
import { NodesOption } from 'app/percona/inventory/Inventory.types';
import { SelectField } from 'app/percona/shared/components/Form/SelectFieldCore';
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
import { nodesOptionsMapper } from 'app/percona/shared/core/reducers/nodes';
Expand Down Expand Up @@ -45,28 +41,23 @@ export const NodesAgents: FC<NodesAgentsProps> = ({ form }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const changeAgentValue = (value: AgentsOption) => {
if (!form?.getState().values?.address) {
if (value.label !== PMM_SERVER_NODE_AGENT_ID) {
form?.change('address', 'localhost');
} else {
form?.change('address', '');
}
// A service monitored by the PMM Server node itself is a remote one, so its address is left for
// the user to fill in. Any other node runs next to what it monitors, hence the localhost default.
const prefillAddress = (node?: NodesOption) => {
if (node && !form?.getState().values?.address) {
form?.change('address', node.isPMMServerNode ? '' : 'localhost');
}
};

const setNodeAndAgent = (value: NodesOption) => {
form?.change('node', value);

let selectedAgent: AgentsOption | undefined;
if (value.agents && value.agents?.length > 1) {
selectedAgent = value.agents.find((item) => item.value === PMM_SERVER_NODE_AGENT_ID);
} else if (value.agents && value.agents?.length === 1) {
selectedAgent = value.agents[0];
}
// A node running several pmm-agents is ambiguous, so the agent is left for the user to pick.
const selectedAgent = value.agents?.length === 1 ? value.agents[0] : undefined;

if (selectedAgent) {
form?.change('pmm_agent_id', selectedAgent);
changeAgentValue(selectedAgent);
prefillAddress(value);
} else {
form?.change('pmm_agent_id', undefined);
}
Expand All @@ -76,13 +67,13 @@ 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) ||
nodesOptions.find((node) => node.isPMMServerNode);
// PMM Server reports the nodes it does not want monitoring delegated to, and they are already
// filtered out. Whatever is left is eligible, the PMM Server node being the natural default
// where it is still offered - a single-node deployment has no other node to pick.
const preselectedNode = nodesOptions.find((node) => node.isPMMServerNode) ?? nodesOptions[0];

if (pmmServerNode) {
setNodeAndAgent(pmmServerNode);
if (preselectedNode) {
setNodeAndAgent(preselectedNode);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -112,7 +103,7 @@ export const NodesAgents: FC<NodesAgentsProps> = ({ form }) => {
options={selectedNode?.agents || []}
name="pmm_agent_id"
data-testid="agents-selectbox"
onChange={(event) => changeAgentValue(event as AgentsOption)}
onChange={() => prefillAddress(selectedNode)}
className={styles.selectField}
aria-label={Messages.form.labels.nodesAgents.agents}
validators={selectedNode ? [validators.required] : undefined}
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 as PMM Server reports it: the PMM Server Nodes and the Nodes of PMM's
// own PostgreSQL cluster are internal, leaving the pre-provisioned PMM Client to be monitored with.
export const nodesMockHA = [
haNodeMock('pmm-ha-0-id', 'pmm-ha-0', true, true),
haNodeMock('pmm-ha-1-id', 'pmm-ha-1', true, true),
haNodeMock('pmm-ha-2-id', 'pmm-ha-2', true, true),
haNodeMock('pmm-ha-client-0-id', 'pmm-pmm-ha-client-0', 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