Skip to content

Commit

Permalink
[Fleet] Display warning when trying to upgrade agent to version > max…
Browse files Browse the repository at this point in the history
… fleet server version (#178079)

Closes [173727](#173727)
## Summary

Adding a warning to the agent upgrade modal: when the selected version
is > than the greatest installed fleet server agent, a warning is
presented to the user.

This happens both for a single agent upgrade and for a bulk upgrade. The
submit button is only disabled in the case of a single agent upgrade to
avoid blocking the user from doing a bulk upgrade. In that case the
endpoint will respond with the same error.

Also adding a link to agent upgrade docs and opened a request to improve
the docs: elastic/ingest-docs#966

![Screenshot 2024-03-11 at 10 39
49](https://github.com/elastic/kibana/assets/16084106/5f3765b6-af25-4fa1-99fd-993d561d0f3b)

![Screenshot 2024-03-08 at 17 17
26](https://github.com/elastic/kibana/assets/16084106/1d0cfe26-8d6e-4937-86da-a53072069450)

Warning for "not upgradeable agent":
![Screenshot 2024-03-11 at 10 50
10](https://github.com/elastic/kibana/assets/16084106/249c69c1-0e38-4c73-b2bf-8122a562da49)




### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 11, 2024
1 parent a918e8f commit f3f2315
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 108 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export class MessageSigningError extends FleetError {}
export class FleetActionsError extends FleetError {}
export class FleetActionsClientError extends FleetError {}
export class UninstallTokenError extends FleetError {}

export class AgentRequestInvalidError extends FleetError {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { checkFleetServerVersion } from './check_fleet_server_versions';

describe('checkFleetServerVersion', () => {
it('should not throw if no force is specified and patch is newer', () => {
const fleetServers = [
{ local_metadata: { elastic: { agent: { version: '8.3.0' } } } },
{ local_metadata: { elastic: { agent: { version: '8.4.0' } } } },
] as any;
expect(() => checkFleetServerVersion('8.4.1', fleetServers, false)).not.toThrowError();
expect(() => checkFleetServerVersion('8.4.1-SNAPSHOT', fleetServers, false)).not.toThrowError();
});

it('should throw if no force is specified and minor is newer', () => {
const fleetServers = [
{ local_metadata: { elastic: { agent: { version: '8.3.0' } } } },
{ local_metadata: { elastic: { agent: { version: '8.4.0' } } } },
] as any;
expect(() => checkFleetServerVersion('8.5.1', fleetServers, false)).toThrowError(
'Cannot upgrade to version 8.5.1 because it is higher than the latest fleet server version 8.4.0.'
);
});

it('should throw if force is specified and patch should not be considered', () => {
const fleetServers = [
{ local_metadata: { elastic: { agent: { version: '8.3.0' } } } },
{ local_metadata: { elastic: { agent: { version: '8.4.0' } } } },
] as any;
expect(() => checkFleetServerVersion('8.5.1', fleetServers, true)).toThrowError(
'Cannot force upgrade to version 8.5.1 because it does not satisfy the major and minor of the latest fleet server version 8.4.0.'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import semverGt from 'semver/functions/gt';
import semverMajor from 'semver/functions/major';
import semverMinor from 'semver/functions/minor';

import type { Agent } from '../types';

import { AgentRequestInvalidError } from '../errors';

import { differsOnlyInPatch } from '.';
import { getMaxVersion } from './get_min_max_version';

// Check the installed fleet server version
export const checkFleetServerVersion = (
versionToUpgradeNumber: string,
fleetServerAgents: Agent[],
force = false
) => {
const message = getFleetServerVersionMessage(versionToUpgradeNumber, fleetServerAgents, force);
if (force && message) throw new AgentRequestInvalidError(message);
if (message) throw new Error(message);
};

export const getFleetServerVersionMessage = (
versionToUpgradeNumber: string | undefined,
fleetServerAgents: Agent[],
force = false
) => {
const fleetServerVersions = fleetServerAgents.map(
(agent) => agent.local_metadata.elastic.agent.version
) as string[];

const maxFleetServerVersion = getMaxVersion(fleetServerVersions);

if (!maxFleetServerVersion || !versionToUpgradeNumber) {
return;
}

if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
) {
return `Cannot upgrade to version ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}.`;
}

const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);

// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
return `Cannot force upgrade to version ${versionToUpgradeNumber} because it does not satisfy the major and minor of the latest fleet server version ${maxFleetServerVersion}.`;
}
};

export const isAgentVersionLessThanFleetServer = (
versionToUpgradeNumber: string | undefined,
fleetServerAgents: Agent[],
force = false
) => {
const fleetServerVersions = fleetServerAgents.map(
(agent) => agent.local_metadata.elastic.agent.version
) as string[];

const maxFleetServerVersion = getMaxVersion(fleetServerVersions);

if (!maxFleetServerVersion || !versionToUpgradeNumber) {
return false;
}
if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
)
return false;

const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);

// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
return false;
}

return true;
};
5 changes: 5 additions & 0 deletions x-pack/plugins/fleet/common/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,8 @@ export {
isAgentPolicyValidForLicense,
unsetAgentPolicyAccordingToLicenseLevel,
} from './agent_policy_config';

export {
getFleetServerVersionMessage,
isAgentVersionLessThanFleetServer,
} from './check_fleet_server_versions';
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useMemo, useCallback } from 'react';

import { useCallback, useState, useMemo } from 'react';
import moment from 'moment';

import {
AGENTS_PREFIX,
FLEET_SERVER_PACKAGE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
SO_SEARCH_LIMIT,
} from '../../../../../../constants';

import { sendGetAgents, sendGetPackagePolicies } from '../../../../../../hooks';

export function useScheduleDateTime(now?: string) {
const initialDatetime = useMemo(() => moment(now), [now]);
const [startDatetime, setStartDatetime] = useState<moment.Moment>(initialDatetime);
Expand Down Expand Up @@ -44,3 +54,27 @@ export function useScheduleDateTime(now?: string) {
maxTime,
};
}

export async function sendAllFleetServerAgents() {
const packagePoliciesRes = await sendGetPackagePolicies({
page: 1,
perPage: SO_SEARCH_LIMIT,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_SERVER_PACKAGE}`,
});
const agentPolicyIds = [
...new Set(packagePoliciesRes?.data?.items.map((p) => p.policy_id) ?? []),
];

if (agentPolicyIds.length === 0) {
return { allFleetServerAgents: [] };
}
const kuery = `${AGENTS_PREFIX}.policy_id:${agentPolicyIds.map((id) => `"${id}"`).join(' or ')}`;

const response = await sendGetAgents({
kuery,
perPage: SO_SEARCH_LIMIT,
showInactive: false,
});

return { allFleetServerAgents: response.data?.items || [] };
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { createFleetTestRendererMock } from '../../../../../../mock';

import { sendGetAgentsAvailableVersions, sendPostBulkAgentUpgrade } from '../../../../hooks';

import { sendAllFleetServerAgents } from './hooks';

import { AgentUpgradeAgentModal } from '.';
import type { AgentUpgradeAgentModalProps } from '.';

Expand All @@ -33,17 +35,24 @@ jest.mock('../../../../hooks', () => {
};
});

jest.mock('./hooks', () => {
return {
...jest.requireActual('./hooks'),
sendAllFleetServerAgents: jest.fn(),
};
});

const mockSendPostBulkAgentUpgrade = sendPostBulkAgentUpgrade as jest.Mock;

const mockSendGetAgentsAvailableVersions = sendGetAgentsAvailableVersions as jest.Mock;
const mockSendAllFleetServerAgents = sendAllFleetServerAgents as jest.Mock;

function renderAgentUpgradeAgentModal(props: Partial<AgentUpgradeAgentModalProps>) {
const renderer = createFleetTestRendererMock();

const utils = renderer.render(
<AgentUpgradeAgentModal agents="" agentCount={12} onClose={() => {}} {...props} />
);

return { utils };
}

Expand Down Expand Up @@ -180,10 +189,113 @@ describe('AgentUpgradeAgentModal', () => {
);
expect(optionList.textContent).toEqual(['8.10.4', '8.10.2+build123456789'].join(''));
});

it('should disable submit button and display a warning for a single agent when version is greater than maxFleetServerVersion', async () => {
mockSendGetAgentsAvailableVersions.mockClear();
mockSendGetAgentsAvailableVersions.mockResolvedValue({
data: {
items: ['8.10.4', '8.10.2', '8.9.0', '8.8.0'],
},
});
mockSendAllFleetServerAgents.mockResolvedValue({
allFleetServerAgents: [
{ id: 'fleet-server', local_metadata: { elastic: { agent: { version: '8.9.0' } } } },
] as any,
});

const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
id: 'agent1',
local_metadata: {
elastic: {
agent: { version: '8.8.0', upgradeable: true },
},
host: { hostname: 'host00001' },
},
},
] as any,
agentCount: 1,
});

await waitFor(() => {
const container = utils.getByTestId('agentUpgradeModal.VersionCombobox');
const input = within(container).getByRole<HTMLInputElement>('combobox');
expect(input?.value).toEqual('8.10.2');
expect(
utils.queryAllByText(
/This action will upgrade the agent running on 'host00001' to version 8.10.2. This action can not be undone. Are you sure you wish to continue?/
)
);
expect(
utils.queryByText(
/Cannot upgrade to version 8.10.2 because it is higher than the latest fleet server version 8.9.0./
)
).toBeInTheDocument();
const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).toBeDisabled();
});
});

it('should display a warning for multiple agents when version is greater than maxFleetServerVersion and not disable submit button', async () => {
mockSendGetAgentsAvailableVersions.mockClear();
mockSendGetAgentsAvailableVersions.mockResolvedValue({
data: {
items: ['8.10.4', '8.10.2', '8.9.0', '8.8.0'],
},
});
mockSendAllFleetServerAgents.mockResolvedValue({
allFleetServerAgents: [
{ id: 'fleet-server', local_metadata: { elastic: { agent: { version: '8.9.0' } } } },
] as any,
});

const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
id: 'agent1',
local_metadata: {
elastic: {
agent: { version: '8.8.0', upgradeable: true },
},
host: { hostname: 'host00001' },
},
},
{
id: 'agent2',
local_metadata: {
elastic: {
agent: { version: '8.8.1', upgradeable: true },
},
host: { hostname: 'host00002' },
},
},
] as any,
agentCount: 1,
});

await waitFor(() => {
const container = utils.getByTestId('agentUpgradeModal.VersionCombobox');
const input = within(container).getByRole<HTMLInputElement>('combobox');
expect(input?.value).toEqual('8.10.2');
expect(
utils.queryAllByText(
/This action will upgrade multiple agents to version 8.10.2. This action can not be undone. Are you sure you wish to continue?/
)
);
expect(
utils.queryByText(
/Please choose another version. Cannot upgrade to version 8.10.2 because it is higher than the latest fleet server version 8.9.0./
)
).toBeInTheDocument();
const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).not.toBeDisabled();
});
});
});

describe('restart upgrade', () => {
it('should restart uprade on updating agents if some agents in updating', async () => {
it('should restart upgrade on updating agents if some agents in updating', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{ status: 'updating', upgrade_started_at: '2022-11-21T12:27:24Z', id: 'agent1' },
Expand Down Expand Up @@ -266,10 +378,9 @@ describe('AgentUpgradeAgentModal', () => {
agentCount: 2,
});
await waitFor(() => {
expect(utils.queryByText(/The selected agent is not upgradeable/)).toBeInTheDocument();
expect(
utils.queryByText(
/Reason: agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service./
/The selected agent is not upgradeable: agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service./
)
).toBeInTheDocument();
const el = utils.getByTestId('confirmModalConfirmButton');
Expand Down
Loading

0 comments on commit f3f2315

Please sign in to comment.