Skip to content

Commit

Permalink
[Fleet] allow agent upgrades if patch version is higher than kibana (#…
Browse files Browse the repository at this point in the history
…173167)

## Summary

Closes #168502

Changed the version check to allow agent upgrade if the agent version
has a newer patch than kibana.

To verify:
- change kibana locally to return a mock version `8.11.0`
[here](https://github.com/elastic/kibana/blob/05bfe53cb3a2fe33ecb9eec4a6fcb19a492aaadf/x-pack/plugins/fleet/public/hooks/use_kibana_version.ts#L17)
- enroll an agent version 8.11.0
- verify that the upgrade is allowed to 8.11.1 and 8.11.2
- verify that the upgrade works

<img width="1282" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/0bb0c8ca-ac0f-49c1-b67c-b02d085e7045">
<img width="799" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/7be3b64b-43e5-451a-a630-65cfc2607dab">
<img width="1256" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/4841bc2a-5238-4854-a63e-27761a02f1e3">

Tested the new agent build version by adding a dummy version to the
available_versions API response.
It is showing up for an agent 8.11.1 (same as the mock kibana version):
<img width="775" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/fa02c62d-9399-4c2c-8311-412e5fe03a96">



### Checklist

- [x] [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
  • Loading branch information
juliaElastic authored Dec 13, 2023
1 parent 08f2f7c commit a26eec4
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { act, fireEvent, waitFor } from '@testing-library/react';

import { createFleetTestRendererMock } from '../../../../../../mock';

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

import { AgentUpgradeAgentModal } from '.';
import type { AgentUpgradeAgentModalProps } from '.';
Expand All @@ -34,6 +34,8 @@ jest.mock('../../../../hooks', () => {

const mockSendPostBulkAgentUpgrade = sendPostBulkAgentUpgrade as jest.Mock;

const mockSendGetAgentsAvailableVersions = sendGetAgentsAvailableVersions as jest.Mock;

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

Expand All @@ -45,126 +47,155 @@ function renderAgentUpgradeAgentModal(props: Partial<AgentUpgradeAgentModalProps
}

describe('AgentUpgradeAgentModal', () => {
it('should set the default to Immediately if there is less than 10 agents using kuery', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 3,
describe('maintenance window', () => {
it('should set the default to Immediately if there is less than 10 agents using kuery', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 3,
});

const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('Immediately');
});

const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('Immediately');
});
it('should set the default to Immediately if there is less than 10 agents using selected agents', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [{ id: 'agent1' }, { id: 'agent2' }] as any,
agentCount: 3,
});

it('should set the default to Immediately if there is less than 10 agents using selected agents', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [{ id: 'agent1' }, { id: 'agent2' }] as any,
agentCount: 3,
const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('Immediately');
});

const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('Immediately');
});
it('should set the default to 1 hour if there is more than 10 agents', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 13,
});

it('should set the default to 1 hour if there is more than 10 agents', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 13,
const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('1 hour');
});

const el = utils.getByTestId('agentUpgradeModal.MaintenanceCombobox');
expect(el?.textContent).toBe('1 hour');
});

it('should enable the version combo if agents is a query', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 30,
describe('version combo', () => {
it('should enable the version combo if agents is a query', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 30,
});

const el = utils.getByTestId('agentUpgradeModal.VersionCombobox');
await waitFor(() => {
expect(el.classList.contains('euiComboBox-isDisabled')).toBe(false);
});
});

const el = utils.getByTestId('agentUpgradeModal.VersionCombobox');
await waitFor(() => {
expect(el.classList.contains('euiComboBox-isDisabled')).toBe(false);
});
});
it('should default the version combo to latest agent version', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [{ id: 'agent1', local_metadata: { host: 'abc' } }] as any,
agentCount: 1,
});

it('should default the version combo to latest agent version', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [{ id: 'agent1', local_metadata: { host: 'abc' } }] as any,
agentCount: 1,
const el = utils.getByTestId('agentUpgradeModal.VersionCombobox');
await waitFor(() => {
expect(el.textContent).toEqual('8.10.2');
});
});

const el = utils.getByTestId('agentUpgradeModal.VersionCombobox');
await waitFor(() => {
expect(el.textContent).toEqual('8.10.2');
it('should display available version options', async () => {
mockSendGetAgentsAvailableVersions.mockClear();
mockSendGetAgentsAvailableVersions.mockResolvedValue({
data: {
items: ['8.10.4', '8.10.2+build123456789', '8.10.2', '8.7.0'],
},
});
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
id: 'agent1',
local_metadata: { host: 'abc', elastic: { agent: { version: '8.10.2' } } },
},
] as any,
agentCount: 1,
});
fireEvent.click(await utils.findByTestId('comboBoxToggleListButton'));
const optionList = await utils.findByTestId(
'comboBoxOptionsList agentUpgradeModal.VersionCombobox-optionsList'
);
expect(optionList.textContent).toEqual(['8.10.4', '8.10.2+build123456789'].join(''));
});
});

it('should restart uprade 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' },
{ id: 'agent2' },
] as any,
agentCount: 2,
isUpdating: true,
describe('restart upgrade', () => {
it('should restart uprade 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' },
{ id: 'agent2' },
] as any,
agentCount: 2,
isUpdating: true,
});

const el = utils.getByTestId('confirmModalTitleText');
expect(el.textContent).toEqual('Restart upgrade on 1 out of 2 agents stuck in updating');

const btn = utils.getByTestId('confirmModalConfirmButton');
await waitFor(() => {
expect(btn).toBeEnabled();
});

act(() => {
fireEvent.click(btn);
});

expect(mockSendPostBulkAgentUpgrade.mock.calls.at(-1)[0]).toEqual(
expect.objectContaining({ agents: ['agent1'], force: true })
);
});

const el = utils.getByTestId('confirmModalTitleText');
expect(el.textContent).toEqual('Restart upgrade on 1 out of 2 agents stuck in updating');

const btn = utils.getByTestId('confirmModalConfirmButton');
await waitFor(() => {
expect(btn).toBeEnabled();
});

act(() => {
fireEvent.click(btn);
it('should restart upgrade on updating agents if kuery', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 3,
isUpdating: true,
});

const el = await utils.findByTestId('confirmModalTitleText');
expect(el.textContent).toEqual('Restart upgrade on 2 out of 3 agents stuck in updating');

const btn = utils.getByTestId('confirmModalConfirmButton');
await waitFor(() => {
expect(btn).toBeEnabled();
});

act(() => {
fireEvent.click(btn);
});

expect(mockSendPostBulkAgentUpgrade.mock.calls.at(-1)[0]).toEqual(
expect.objectContaining({
agents:
'(*) AND status:updating AND upgrade_started_at:* AND NOT upgraded_at:* AND upgrade_started_at < now-2h',
force: true,
})
);
});

expect(mockSendPostBulkAgentUpgrade.mock.calls.at(-1)[0]).toEqual(
expect.objectContaining({ agents: ['agent1'], force: true })
);
});

it('should restart upgrade on updating agents if kuery', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: '*',
agentCount: 3,
isUpdating: true,
it('should disable submit button if no agents stuck updating', () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{ status: 'offline', upgrade_started_at: '2022-11-21T12:27:24Z', id: 'agent1' },
{ id: 'agent2' },
] as any,
agentCount: 2,
isUpdating: true,
});

const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).toBeDisabled();
});

const el = await utils.findByTestId('confirmModalTitleText');
expect(el.textContent).toEqual('Restart upgrade on 2 out of 3 agents stuck in updating');

const btn = utils.getByTestId('confirmModalConfirmButton');
await waitFor(() => {
expect(btn).toBeEnabled();
});

act(() => {
fireEvent.click(btn);
});

expect(mockSendPostBulkAgentUpgrade.mock.calls.at(-1)[0]).toEqual(
expect.objectContaining({
agents:
'(*) AND status:updating AND upgrade_started_at:* AND NOT upgraded_at:* AND upgrade_started_at < now-2h',
force: true,
})
);
});

it('should disable submit button if no agents stuck updating', () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{ status: 'offline', upgrade_started_at: '2022-11-21T12:27:24Z', id: 'agent1' },
{ id: 'agent2' },
] as any,
agentCount: 2,
isUpdating: true,
});

const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
useConfig,
sendGetAgentStatus,
useAgentVersion,
differsOnlyInPatch,
} from '../../../../hooks';

import { sendGetAgentsAvailableVersions } from '../../../../hooks';
Expand Down Expand Up @@ -164,7 +165,9 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo

const versionOptions: Array<EuiComboBoxOptionOption<string>> = useMemo(() => {
const displayVersions = minVersion
? availableVersions.filter((v) => semverGt(v, minVersion))
? availableVersions.filter(
(v) => semverGt(v, minVersion) || differsOnlyInPatch(v, minVersion, false)
)
: availableVersions;

const options = displayVersions.map((option) => ({
Expand Down
43 changes: 43 additions & 0 deletions x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ describe('useAgentVersion', () => {
expect(result.current).toEqual(mockKibanaVersion);
});

it('should return agent version with newer patch than kibana', async () => {
const mockKibanaVersion = '8.8.1';
const mockAvailableVersions = ['8.9.0', '8.8.2', '8.8.0', '8.7.0'];

(useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
(sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
data: { items: mockAvailableVersions },
});

const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());

expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();

await waitForNextUpdate();

expect(result.current).toEqual('8.8.2');
});

it('should return the latest availeble agent version if a version that matches Kibana version is not released', async () => {
const mockKibanaVersion = '8.11.0';
const mockAvailableVersions = ['8.8.0', '8.7.0', '8.9.2', '7.16.0'];
Expand Down Expand Up @@ -122,4 +140,29 @@ describe('useAgentVersion', () => {

expect(result.current).toEqual(mockKibanaVersion);
});

it('should return the latest availeble agent version if has build suffix', async () => {
const mockKibanaVersion = '8.11.0';
const mockAvailableVersions = [
'8.12.0',
'8.11.1+build123456789',
'8.8.0',
'8.7.0',
'8.9.2',
'7.16.0',
];

(useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
(sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
data: { items: mockAvailableVersions },
});

const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());

expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();

await waitForNextUpdate();

expect(result.current).toEqual('8.11.1+build123456789');
});
});
19 changes: 16 additions & 3 deletions x-pack/plugins/fleet/public/hooks/use_agent_version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export const useAgentVersion = (): string | undefined => {
const availableVersions = res?.data?.items;
let agentVersionToUse;

availableVersions?.sort(semverRcompare);
if (
availableVersions &&
availableVersions.length > 0 &&
availableVersions.indexOf(kibanaVersion) === -1
availableVersions.indexOf(kibanaVersion) !== 0
) {
availableVersions.sort(semverRcompare);
agentVersionToUse =
availableVersions.find((version) => {
return semverLt(version, kibanaVersion);
return semverLt(version, kibanaVersion) || differsOnlyInPatch(version, kibanaVersion);
}) || availableVersions[0];
} else {
agentVersionToUse = kibanaVersion;
Expand All @@ -50,3 +50,16 @@ export const useAgentVersion = (): string | undefined => {

return agentVersion;
};

export const differsOnlyInPatch = (
versionA: string,
versionB: string,
allowEqualPatch: boolean = true
): boolean => {
const [majorA, minorA, patchA] = versionA.split('.');
const [majorB, minorB, patchB] = versionB.split('.');

return (
majorA === majorB && minorA === minorB && (allowEqualPatch ? patchA >= patchB : patchA > patchB)
);
};

0 comments on commit a26eec4

Please sign in to comment.