From a26eec451fb54317a2b08d1f49b3f2b30fcbddca Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Wed, 13 Dec 2023 17:00:20 +0100
Subject: [PATCH] [Fleet] allow agent upgrades if patch version is higher than
kibana (#173167)
## Summary
Closes https://github.com/elastic/kibana/issues/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
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):
### 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
---
.../agent_upgrade_modal/index.test.tsx | 231 ++++++++++--------
.../components/agent_upgrade_modal/index.tsx | 5 +-
.../public/hooks/use_agent_version.test.ts | 43 ++++
.../fleet/public/hooks/use_agent_version.ts | 19 +-
4 files changed, 194 insertions(+), 104 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
index 11dcd1a34bb2a..7850ae3a3128d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.test.tsx
@@ -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 '.';
@@ -34,6 +34,8 @@ jest.mock('../../../../hooks', () => {
const mockSendPostBulkAgentUpgrade = sendPostBulkAgentUpgrade as jest.Mock;
+const mockSendGetAgentsAvailableVersions = sendGetAgentsAvailableVersions as jest.Mock;
+
function renderAgentUpgradeAgentModal(props: Partial) {
const renderer = createFleetTestRendererMock();
@@ -45,126 +47,155 @@ function renderAgentUpgradeAgentModal(props: Partial {
- 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();
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
index d361349b3f327..0acb4296befdc 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx
@@ -44,6 +44,7 @@ import {
useConfig,
sendGetAgentStatus,
useAgentVersion,
+ differsOnlyInPatch,
} from '../../../../hooks';
import { sendGetAgentsAvailableVersions } from '../../../../hooks';
@@ -164,7 +165,9 @@ export const AgentUpgradeAgentModal: React.FunctionComponent> = 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) => ({
diff --git a/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts b/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
index 6cb1c8ee42248..34a7afe33baaa 100644
--- a/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
@@ -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'];
@@ -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');
+ });
});
diff --git a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
index 8c198dbc7773e..85409759cd88a 100644
--- a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
@@ -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;
@@ -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)
+ );
+};