Skip to content

Commit

Permalink
[Fleet] Display message explaining why agent is not upgradeable (#173253
Browse files Browse the repository at this point in the history
)

Closes #171840
Also implements the UI part of
#173281

## Summary
When trying to upgrade a single agent that is not upgradeable, the error
message doesn't specify _why_ the agent cannot be upgraded. This PR is
introducing:
- More granular error messages in the endpoint `POST
agent/{agent_id}/upgrade`. The messages are now different depending on
which conditions are met. For the case when the agent is reported not
upgradeable from elastic agent, there is now an error message the states
it explicitly.
- When clicking on the `upgrade 1 agent` from the bulk actions, if the
agent is not upgradeable the submit button is now greyed out and a
message explaining the reason is shown:
- 
![Screenshot 2023-12-18 at 14 41
48](https://github.com/elastic/kibana/assets/16084106/8e0a03b4-c8fc-4a2e-aea6-77c7cc1acdf3)

The same warning appears when clicking on other upgrade actions in the
bulk action menu, but only for a single agent. Multiple upgrades have
not been changed.

- In the agents list, reuse the existing tooltip besides the version to
show the same messages when the agent is not upgradeable:
![Screenshot 2023-12-18 at 14 40
32](https://github.com/elastic/kibana/assets/16084106/ada30bc0-8c58-40d9-b7cf-c5c7a81a75f7)




### Checklist
- [ ] [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

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
criamico and kibanamachine committed Dec 19, 2023
1 parent 8ff9198 commit 63f4e38
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 41 deletions.
149 changes: 149 additions & 0 deletions x-pack/plugins/fleet/common/services/is_agent_upgradeable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getRecentUpgradeInfoForAgent,
isAgentUpgradeable,
isAgentUpgrading,
getNotUpgradeableMessage,
} from './is_agent_upgradeable';

const getAgent = ({
Expand Down Expand Up @@ -241,6 +242,154 @@ describe('Fleet - isAgentUpgradeable', () => {
});
});

describe('Fleet - getNotUpgradeableMessage', () => {
it('if agent reports not upgradeable with agent version < latest agent version', () => {
expect(getNotUpgradeableMessage(getAgent({ version: '7.9.0' }), '8.0.0')).toBe(
'agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service.'
);
});

it('if agent reports not upgradeable with agent version > latest agent version', () => {
expect(getNotUpgradeableMessage(getAgent({ version: '8.0.0' }), '7.9.0')).toBe(
'agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service.'
);
});

it('returns false if agent reports not upgradeable with agent version === latest agent version', () => {
expect(getNotUpgradeableMessage(getAgent({ version: '8.0.0' }), '8.0.0')).toBe(
'agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service.'
);
});

it('if agent reports upgradeable, with agent version === latest agent version', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '8.0.0', upgradeable: true }), '8.0.0')
).toBe('agent is already running on the latest available version.');
});

it('if agent reports upgradeable, with agent version > latest agent version', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '8.0.0', upgradeable: true }), '7.9.0')
).toBe('agent is running on a version greater than the latest available version.');
});

it('if agent reports upgradeable, but agent is unenrolling', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '7.9.0', upgradeable: true, unenrolling: true }),
'8.0.0'
)
).toBe('agent is being unenrolled.');
});

it('if agent reports upgradeable, but agent is unenrolled', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '7.9.0', upgradeable: true, unenrolled: true }),
'8.0.0'
)
).toBe('agent has been unenrolled.');
});

it('Returns no error message if agent reports upgradeable, with agent version < latest agent version', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0')
).toBeUndefined();
});

it('if agent reports upgradeable, with agent snapshot version === latest agent version', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '7.9.0-SNAPSHOT', upgradeable: true }), '7.9.0')
).toBe('agent is already running on the latest available version.');
});

it('it does not return message if agent reports upgradeable, with upgrade to agent snapshot version newer than latest agent version', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '8.10.2', upgradeable: true }),
'8.10.2',
'8.11.0-SNAPSHOT'
)
).toBeUndefined();
});

it('if agent reports upgradeable, with target version < current agent version ', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0', '7.8.0')
).toBe('agent does not support downgrades.');
});

it('if agent reports upgradeable, with target version == current agent version ', () => {
expect(
getNotUpgradeableMessage(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0', '7.9.0')
).toBe('agent is already running on the selected version.');
});

it('if agent with no upgrade details reports upgradeable, but is already upgrading', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '7.9.0', upgradeable: true, upgrading: true }),
'8.0.0'
)
).toBe('agent is already being upgraded.');
});

it('if agent reports upgradeable, but has an upgrade status other than failed', () => {
expect(
getNotUpgradeableMessage(
getAgent({
version: '7.9.0',
upgradeable: true,
upgradeDetails: {
target_version: '8.0.0',
action_id: 'XXX',
state: 'UPG_REQUESTED',
},
}),
'8.0.0'
)
).toBe('agent is already being upgraded.');
});

it('it does not return a message if agent reports upgradeable and has a failed upgrade status', () => {
expect(
getNotUpgradeableMessage(
getAgent({
version: '7.9.0',
upgradeable: true,
upgradeDetails: {
target_version: '8.0.0',
action_id: 'XXX',
state: 'UPG_FAILED',
metadata: {
error_msg: 'Upgrade timed out',
},
},
}),
'8.0.0'
)
).toBeUndefined();
});

it('if the agent reports upgradeable but was upgraded less than 10 minutes ago', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 9 }),
'8.0.0'
)
).toContain('please wait');
});

it('if agent reports upgradeable and was upgraded more than 10 minutes ago', () => {
expect(
getNotUpgradeableMessage(
getAgent({ version: '7.9.0', upgradeable: true, minutesSinceUpgrade: 11 }),
'8.0.0'
)
).toBeUndefined();
});
});

describe('hasAgentBeenUpgradedRecently', () => {
it('returns true if the agent was upgraded less than 10 minutes ago', () => {
expect(
Expand Down
95 changes: 87 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 @@ -8,11 +8,27 @@
import semverCoerce from 'semver/functions/coerce';
import semverLt from 'semver/functions/lt';
import semverGt from 'semver/functions/gt';
import semverEq from 'semver/functions/eq';
import moment from 'moment';

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

export const AGENT_UPGRADE_COOLDOWN_IN_MIN = 10;

// Error messages for agent not upgradeable
export const VERSION_MISSING_ERROR = `agent version is missing.`;
export const UNENROLLED_ERROR = `agent has been unenrolled.`;
export const ONGOING_UNEROLLMENT_ERROR = `agent is being unenrolled.`;
export const NOT_UPGRADEABLE_ERROR = `agent cannot be upgraded through Fleet. It may be running in a container or it is not installed as a service.`;
export const ALREADY_UPGRADED_ERROR = `agent is already being upgraded.`;
export const INVALID_VERSION_ERROR = 'agent version is not valid.';
export const SELECTED_VERSION_ERROR = 'the selected version is not valid.';
export const RUNNING_SELECTED_VERSION_ERROR = `agent is already running on the selected version.`;
export const DOWNGRADE_NOT_ALLOWED_ERROR = `agent does not support downgrades.`;
export const LATEST_VERSION_NOT_VALID_ERROR = 'latest version is not valid.';
export const AGENT_ALREADY_ON_LATEST_ERROR = `agent is already running on the latest available version.`;
export const AGENT_ON_GREATER_VERSION_ERROR = `agent is running on a version greater than the latest available version.`;

export function isAgentUpgradeable(
agent: Agent,
latestAgentVersion: string,
Expand Down Expand Up @@ -42,41 +58,104 @@ export function isAgentUpgradeable(
return isAgentVersionLessThanLatest(agentVersion, latestAgentVersion);
}

// Based on the previous, returns a detailed message explaining why the agent is not upgradeable
export const getNotUpgradeableMessage = (
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;
} else {
return VERSION_MISSING_ERROR;
}
if (agent.unenrolled_at) {
return UNENROLLED_ERROR;
}
if (agent.unenrollment_started_at) {
return ONGOING_UNEROLLMENT_ERROR;
}
if (!agent.local_metadata.elastic.agent.upgradeable) {
return NOT_UPGRADEABLE_ERROR;
}
if (isAgentUpgrading(agent)) {
return ALREADY_UPGRADED_ERROR;
}
if (getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently) {
const timeToWaitMins = getRecentUpgradeInfoForAgent(agent).timeToWaitMins;
const elapsedMinsSinceUpgrade = getRecentUpgradeInfoForAgent(agent).elapsedMinsSinceUpgrade;
return `agent was upgraded ${elapsedMinsSinceUpgrade} minutes ago, please wait ${timeToWaitMins} minutes before attempting the upgrade again.`;
}
const agentVersionNumber = semverCoerce(agentVersion);
if (!agentVersionNumber) return INVALID_VERSION_ERROR;

if (versionToUpgrade !== undefined) {
const versionToUpgradeNumber = semverCoerce(versionToUpgrade);
if (!versionToUpgradeNumber) return SELECTED_VERSION_ERROR;

if (semverEq(agentVersionNumber, versionToUpgradeNumber)) return RUNNING_SELECTED_VERSION_ERROR;

if (semverLt(versionToUpgradeNumber, agentVersionNumber)) return DOWNGRADE_NOT_ALLOWED_ERROR;

// explicitly allow this case - the agent is upgradeable
if (semverGt(versionToUpgradeNumber, agentVersionNumber)) return undefined;
}

const latestAgentVersionNumber = semverCoerce(latestAgentVersion);
if (!latestAgentVersionNumber) return LATEST_VERSION_NOT_VALID_ERROR;

if (semverEq(agentVersionNumber, latestAgentVersionNumber)) return AGENT_ALREADY_ON_LATEST_ERROR;

if (semverGt(agentVersionNumber, latestAgentVersionNumber)) return AGENT_ON_GREATER_VERSION_ERROR;

// in all the other cases, the agent is upgradeable; don't return any message.
return undefined;
};

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');
if (!agentVersionNumber) throw new Error(`${INVALID_VERSION_ERROR}`);
const latestAgentVersionNumber = semverCoerce(latestAgentVersion);
if (!latestAgentVersionNumber) throw new Error('latest version is not valid');
if (!latestAgentVersionNumber) throw new Error(`${LATEST_VERSION_NOT_VALID_ERROR}`);

return semverLt(agentVersionNumber, latestAgentVersionNumber);
};

const isNotDowngrade = (agentVersion: string, versionToUpgrade: string) => {
const agentVersionNumber = semverCoerce(agentVersion);
if (!agentVersionNumber) throw new Error('agent version is not valid');
if (!agentVersionNumber) throw new Error(`${INVALID_VERSION_ERROR}`);
const versionToUpgradeNumber = semverCoerce(versionToUpgrade);
if (!versionToUpgradeNumber) throw new Error('target version is not valid');
if (!versionToUpgradeNumber) throw new Error(`${SELECTED_VERSION_ERROR}`);

return semverGt(versionToUpgradeNumber, agentVersionNumber);
};

export function getRecentUpgradeInfoForAgent(agent: Agent): {
hasBeenUpgradedRecently: boolean;
timeToWaitMs: number;
elapsedMinsSinceUpgrade: number;
timeToWaitMins: number;
} {
if (!agent.upgraded_at) {
return {
hasBeenUpgradedRecently: false,
timeToWaitMs: 0,
timeToWaitMins: 0,
elapsedMinsSinceUpgrade: 0,
};
}

const elaspedSinceUpgradeInMillis = Date.now() - Date.parse(agent.upgraded_at);
const timeToWaitMs = AGENT_UPGRADE_COOLDOWN_IN_MIN * 6e4 - elaspedSinceUpgradeInMillis;
const hasBeenUpgradedRecently = elaspedSinceUpgradeInMillis / 6e4 < AGENT_UPGRADE_COOLDOWN_IN_MIN;
const elapsedSinceUpgradeInMillis = Date.now() - Date.parse(agent.upgraded_at);
const elapsedMins = moment.duration(elapsedSinceUpgradeInMillis, 'milliseconds').asMinutes();
const elapsedMinsSinceUpgrade = Math.ceil(elapsedMins);

return { hasBeenUpgradedRecently, timeToWaitMs };
const timeToWaitMs = AGENT_UPGRADE_COOLDOWN_IN_MIN * 6e4 - elapsedSinceUpgradeInMillis;
const hasBeenUpgradedRecently = elapsedSinceUpgradeInMillis / 6e4 < AGENT_UPGRADE_COOLDOWN_IN_MIN;
const timeToWait = moment.duration(timeToWaitMs, 'milliseconds').asMinutes();
const timeToWaitMins = Math.ceil(timeToWait);
return { hasBeenUpgradedRecently, timeToWaitMs, elapsedMinsSinceUpgrade, timeToWaitMins };
}

export function isAgentUpgrading(agent: Agent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { Tags } from '../../components/tags';
import type { AgentMetrics } from '../../../../../../../common/types';
import { formatAgentCPU, formatAgentMemory } from '../../services/agent_metrics';

import { getNotUpgradeableMessage } from '../../../../../../../common/services/is_agent_upgradeable';

import { AgentUpgradeStatus } from './agent_upgrade_status';

import { EmptyPrompt } from './empty_prompt';
Expand Down Expand Up @@ -303,6 +305,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
agentUpgradeStartedAt={agent.upgrade_started_at}
agentUpgradedAt={agent.upgraded_at}
agentUpgradeDetails={agent.upgrade_details}
notUpgradeableMessage={getNotUpgradeableMessage(agent, latestAgentVersion)}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,39 @@ export const AgentUpgradeStatus: React.FC<{
agentUpgradeStartedAt?: string | null;
agentUpgradedAt?: string | null;
agentUpgradeDetails?: AgentUpgradeDetails;
}> = ({ isAgentUpgradable, agentUpgradeStartedAt, agentUpgradedAt, agentUpgradeDetails }) => {
notUpgradeableMessage?: string | null;
}> = ({
isAgentUpgradable,
agentUpgradeStartedAt,
agentUpgradedAt,
agentUpgradeDetails,
notUpgradeableMessage,
}) => {
const isAgentUpgrading = useMemo(
() => agentUpgradeStartedAt && !agentUpgradedAt,
[agentUpgradeStartedAt, agentUpgradedAt]
);
const status = useMemo(() => getStatusComponents(agentUpgradeDetails), [agentUpgradeDetails]);
const minVersion = '8.12';

if (!isAgentUpgradable && notUpgradeableMessage) {
return (
<EuiIconTip
type="iInCircle"
content={
<FormattedMessage
id="xpack.fleet.agentUpgradeStatusBadge.notUpgradeable"
defaultMessage="Agent not upgradeable: {reason}"
values={{
reason: notUpgradeableMessage,
}}
/>
}
color="subdued"
/>
);
}

if (isAgentUpgradable) {
return (
<EuiBadge color="hollow" iconType="sortUp">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jest.mock('../../../../hooks', () => {
}),
sendPostBulkAgentUpgrade: jest.fn(),
useAgentVersion: jest.fn().mockReturnValue('8.10.2'),
useKibanaVersion: jest.fn().mockReturnValue('8.10.2'),
};
});

Expand Down Expand Up @@ -203,4 +204,28 @@ describe('AgentUpgradeAgentModal', () => {
expect(el).toBeDisabled();
});
});

it('should disable submit button and display a warning for a single agent that is not upgradeable', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
status: 'offline',
upgrade_started_at: '2022-11-21T12:27:24Z',
id: 'agent1',
local_metadata: { elastic: { agent: { version: '8.9.0' } } },
},
] as any,
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./
)
).toBeInTheDocument();
const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).toBeDisabled();
});
});
});
Loading

0 comments on commit 63f4e38

Please sign in to comment.