Skip to content

Commit

Permalink
[Fleet] Agent upgrade available should use latest agent version (#167410
Browse files Browse the repository at this point in the history
)

## Summary

Closes #167387

Replaced using kibana version when deciding if agent upgrade is
available (only in serverless, in stateful kibana version is still
returned as an available version).

To verify locally:
- [to test stateless] add this to `kibana.dev.yml`:
`xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions: true`
- extract the `agent_versions_list.json` to local kibana folder
`~/kibana/x-pack/plugins/fleet/target`

[agent_versions_list.json.zip](https://github.com/elastic/kibana/files/12739519/agent_versions_list.json.zip)
- verify that upgrade available warnings still work if agent is < latest
agent version (8.10.2)
- when trying to upgrade agent, verify that the default version is the
latest agent version, and 8.11 is not in the list

Agent list:
<img width="1475" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/f06b7bc8-97e6-4ff9-b872-736ede5e969a">

Upgrade available filter - 1 agent on latest version, 9 upgradeable:
<img width="1314" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/4ff5ac02-903b-493b-94df-68b1b7ad6846">

Agent details:
<img width="1512" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/3ff6e1d5-2ccc-4814-83e5-c4760ad63722">

Agent on latest version has disable `Upgrade agent` action:
<img width="1322" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/f461dbf5-04e5-4bcc-8801-48c2b1a90225">

Bulk action with one agent that is not upgradeable (already on latest
version), expected error:
<img width="1597" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/8bfa46ae-6684-4748-9fca-e908c142b642">




### 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 Sep 29, 2023
1 parent c7bb851 commit 474c8ea
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 64 deletions.
20 changes: 12 additions & 8 deletions x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import semverGt from 'semver/functions/gt';

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

export function isAgentUpgradeable(agent: Agent, kibanaVersion: string, versionToUpgrade?: string) {
export function isAgentUpgradeable(
agent: Agent,
latestAgentVersion: string,
versionToUpgrade?: string
) {
let agentVersion: string;
if (typeof agent?.local_metadata?.elastic?.agent?.version === 'string') {
agentVersion = agent.local_metadata.elastic.agent.version;
Expand All @@ -31,23 +35,23 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string, versionT
if (versionToUpgrade !== undefined) {
return (
isNotDowngrade(agentVersion, versionToUpgrade) &&
isAgentVersionLessThanKibana(agentVersion, kibanaVersion)
isAgentVersionLessThanLatest(agentVersion, latestAgentVersion)
);
}
return isAgentVersionLessThanKibana(agentVersion, kibanaVersion);
return isAgentVersionLessThanLatest(agentVersion, latestAgentVersion);
}

export const isAgentVersionLessThanKibana = (agentVersion: string, kibanaVersion: string) => {
const isAgentVersionLessThanLatest = (agentVersion: string, latestAgentVersion: string) => {
// make sure versions are only the number before comparison
const agentVersionNumber = semverCoerce(agentVersion);
if (!agentVersionNumber) throw new Error('agent version is not valid');
const kibanaVersionNumber = semverCoerce(kibanaVersion);
if (!kibanaVersionNumber) throw new Error('kibana version is not valid');
const latestAgentVersionNumber = semverCoerce(latestAgentVersion);
if (!latestAgentVersionNumber) throw new Error('latest version is not valid');

return semverLt(agentVersionNumber, kibanaVersionNumber);
return semverLt(agentVersionNumber, latestAgentVersionNumber);
};

export const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => {
const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => {
const agentVersionNumber = semverCoerce(agentVersion);
if (!agentVersionNumber) throw new Error('agent version is not valid');
const versionToUpgradeNumber = semverCoerce(versionToUpgrade);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import { createFleetTestRendererMock } from '../../../../../../mock';
import type { Agent, AgentPolicy } from '../../../../types';
import { ExperimentalFeaturesService } from '../../../../services';
import { useAuthz } from '../../../../../../hooks/use_authz';
import { useAgentVersion } from '../../../../../../hooks/use_agent_version';

import { AgentDetailsActionMenu } from './actions_menu';

jest.mock('../../../../../../services/experimental_features');
jest.mock('../../../../../../hooks/use_authz');
jest.mock('../../../../../../hooks/use_agent_version');

const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);
const mockedUseAuthz = jest.mocked(useAuthz);
const mockedUseAgentVersion = jest.mocked(useAgentVersion);

function renderActions({ agent, agentPolicy }: { agent: Agent; agentPolicy?: AgentPolicy }) {
const renderer = createFleetTestRendererMock();
Expand Down Expand Up @@ -48,6 +51,7 @@ describe('AgentDetailsActionMenu', () => {
all: true,
},
} as any);
mockedUseAgentVersion.mockReturnValue('8.10.2');
});

describe('Request Diagnotics action', () => {
Expand Down Expand Up @@ -162,45 +166,90 @@ describe('AgentDetailsActionMenu', () => {
expect(res).toBeEnabled();
});
});
});

describe('Restart upgrade action', () => {
function renderAndGetRestartUpgradeButton({
agent,
agentPolicy,
}: {
agent: Agent;
agentPolicy?: AgentPolicy;
}) {
const { utils } = renderActions({
describe('Restart upgrade action', () => {
function renderAndGetRestartUpgradeButton({
agent,
agentPolicy,
});
}: {
agent: Agent;
agentPolicy?: AgentPolicy;
}) {
const { utils } = renderActions({
agent,
agentPolicy,
});

return utils.queryByTestId('restartUpgradeBtn');
}
return utils.queryByTestId('restartUpgradeBtn');
}

it('should render an active button', async () => {
const res = renderAndGetRestartUpgradeButton({
agent: {
status: 'updating',
upgrade_started_at: '2022-11-21T12:27:24Z',
} as any,
agentPolicy: {} as AgentPolicy,
it('should render an active button', async () => {
const res = renderAndGetRestartUpgradeButton({
agent: {
status: 'updating',
upgrade_started_at: '2022-11-21T12:27:24Z',
} as any,
agentPolicy: {} as AgentPolicy,
});

expect(res).not.toBe(null);
expect(res).toBeEnabled();
});

expect(res).not.toBe(null);
expect(res).toBeEnabled();
it('should not render action if agent is not stuck in updating', async () => {
const res = renderAndGetRestartUpgradeButton({
agent: {
status: 'updating',
upgrade_started_at: new Date().toISOString(),
} as any,
agentPolicy: {} as AgentPolicy,
});
expect(res).toBe(null);
});
});

it('should not render action if agent is not stuck in updating', async () => {
const res = renderAndGetRestartUpgradeButton({
agent: {
status: 'updating',
upgrade_started_at: new Date().toISOString(),
} as any,
agentPolicy: {} as AgentPolicy,
describe('Upgrade action', () => {
function renderAndGetUpgradeButton({
agent,
agentPolicy,
}: {
agent: Agent;
agentPolicy?: AgentPolicy;
}) {
const { utils } = renderActions({
agent,
agentPolicy,
});

return utils.queryByTestId('upgradeBtn');
}

it('should render an active action button if agent version is not the latest', async () => {
const res = renderAndGetUpgradeButton({
agent: {
active: true,
status: 'online',
local_metadata: { elastic: { agent: { version: '8.8.0', upgradeable: true } } },
} as any,
agentPolicy: {} as AgentPolicy,
});

expect(res).not.toBe(null);
expect(res).toBeEnabled();
});

it('should render a disabled action button if agent version is latest', async () => {
const res = renderAndGetUpgradeButton({
agent: {
active: true,
status: 'online',
local_metadata: { elastic: { agent: { version: '8.10.2', upgradeable: true } } },
} as any,
agentPolicy: {} as AgentPolicy,
});

expect(res).not.toBe(null);
expect(res).not.toBeEnabled();
});
expect(res).toBe(null);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isAgentRequestDiagnosticsSupported } from '../../../../../../../common/
import { isStuckInUpdating } from '../../../../../../../common/services/agent_status';

import type { Agent, AgentPolicy } from '../../../../types';
import { useAuthz, useKibanaVersion } from '../../../../hooks';
import { useAgentVersion, useAuthz } from '../../../../hooks';
import { ContextMenuActions } from '../../../../components';
import {
AgentUnenrollAgentModal,
Expand All @@ -34,7 +34,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
onCancelReassign?: () => void;
}> = memo(({ agent, assignFlyoutOpenByDefault = false, onCancelReassign, agentPolicy }) => {
const hasFleetAllPrivileges = useAuthz().fleet.all;
const kibanaVersion = useKibanaVersion();
const latestAgentVersion = useAgentVersion();
const refreshAgent = useAgentRefresh();
const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault);
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
Expand Down Expand Up @@ -102,11 +102,12 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="refresh"
disabled={!isAgentUpgradeable(agent, kibanaVersion)}
disabled={!!latestAgentVersion && !isAgentUpgradeable(agent, latestAgentVersion)}
onClick={() => {
setIsUpgradeModalOpen(true);
}}
key="upgradeAgent"
data-test-subj="upgradeBtn"
>
<FormattedMessage
id="xpack.fleet.agentList.upgradeOneButton"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';

import type { Agent, AgentPolicy } from '../../../../../types';
import { useKibanaVersion } from '../../../../../hooks';
import { useAgentVersion } from '../../../../../hooks';
import { ExperimentalFeaturesService, isAgentUpgradeable } from '../../../../../services';
import { AgentPolicySummaryLine } from '../../../../../components';
import { AgentHealth } from '../../../components';
Expand All @@ -39,7 +39,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
agent: Agent;
agentPolicy?: AgentPolicy;
}> = memo(({ agent, agentPolicy }) => {
const kibanaVersion = useKibanaVersion();
const latestAgentVersion = useAgentVersion();
const { displayAgentMetrics } = ExperimentalFeaturesService.get();

return (
Expand Down Expand Up @@ -173,7 +173,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
<EuiFlexItem grow={false} className="eui-textNoWrap">
{agent.local_metadata.elastic.agent.version}
</EuiFlexItem>
{isAgentUpgradeable(agent, kibanaVersion) ? (
{latestAgentVersion && isAgentUpgradeable(agent, latestAgentVersion) ? (
<EuiFlexItem grow={false}>
<EuiToolTip
position="right"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { isAgentUpgradeable, ExperimentalFeaturesService } from '../../../../ser
import { AgentHealth } from '../../components';

import type { Pagination } from '../../../../hooks';
import { useLink, useKibanaVersion, useAuthz } from '../../../../hooks';
import { useAgentVersion } from '../../../../hooks';
import { useLink, useAuthz } from '../../../../hooks';

import { AgentPolicySummaryLine } from '../../../../components';
import { Tags } from '../../components/tags';
Expand Down Expand Up @@ -91,7 +92,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
const { displayAgentMetrics } = ExperimentalFeaturesService.get();

const { getHref } = useLink();
const kibanaVersion = useKibanaVersion();
const latestAgentVersion = useAgentVersion();

const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;
Expand Down Expand Up @@ -284,7 +285,9 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
<EuiFlexItem grow={false} className="eui-textNoWrap">
{safeMetadata(version)}
</EuiFlexItem>
{isAgentSelectable(agent) && isAgentUpgradeable(agent, kibanaVersion) ? (
{isAgentSelectable(agent) &&
latestAgentVersion &&
isAgentUpgradeable(agent, latestAgentVersion) ? (
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs" className="eui-textNoWrap">
<EuiIcon size="m" type="warning" color="warning" />
Expand Down Expand Up @@ -324,7 +327,10 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
totalAgents
? showUpgradeable
? agents.filter(
(agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent, kibanaVersion)
(agent) =>
isAgentSelectable(agent) &&
latestAgentVersion &&
isAgentUpgradeable(agent, latestAgentVersion)
)
: agents
: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import { createFleetTestRendererMock } from '../../../../../../mock';
import type { Agent, AgentPolicy } from '../../../../types';
import { ExperimentalFeaturesService } from '../../../../services';
import { useAuthz } from '../../../../../../hooks/use_authz';
import { useAgentVersion } from '../../../../../../hooks/use_agent_version';

import { TableRowActions } from './table_row_actions';

jest.mock('../../../../../../services/experimental_features');
jest.mock('../../../../../../hooks/use_authz');
jest.mock('../../../../../../hooks/use_agent_version');

const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);
const mockedUseAuthz = jest.mocked(useAuthz);
const mockedUseAgentVersion = jest.mocked(useAgentVersion);

function renderTableRowActions({
agent,
Expand Down Expand Up @@ -57,6 +60,7 @@ describe('TableRowActions', () => {
all: true,
},
} as any);
mockedUseAgentVersion.mockReturnValue('8.10.2');
});

describe('Request Diagnotics action', () => {
Expand Down Expand Up @@ -179,4 +183,53 @@ describe('TableRowActions', () => {
expect(res).toBe(null);
});
});

describe('Upgrade action', () => {
function renderAndGetUpgradeButton({
agent,
agentPolicy,
}: {
agent: Agent;
agentPolicy?: AgentPolicy;
}) {
const { utils } = renderTableRowActions({
agent,
agentPolicy,
});

return utils.queryByTestId('upgradeBtn');
}

it('should render an active action button if agent version is not the latest', async () => {
const res = renderAndGetUpgradeButton({
agent: {
active: true,
status: 'online',
local_metadata: { elastic: { agent: { version: '8.8.0', upgradeable: true } } },
} as any,
agentPolicy: {
is_managed: false,
} as AgentPolicy,
});

expect(res).not.toBe(null);
expect(res).toBeEnabled();
});

it('should render a disabled action button if agent version is latest', async () => {
const res = renderAndGetUpgradeButton({
agent: {
active: true,
status: 'online',
local_metadata: { elastic: { agent: { version: '8.10.2', upgradeable: true } } },
} as any,
agentPolicy: {
is_managed: false,
} as AgentPolicy,
});

expect(res).not.toBe(null);
expect(res).not.toBeEnabled();
});
});
});
Loading

0 comments on commit 474c8ea

Please sign in to comment.