Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Display message explaining why agent is not upgradeable #173253

Merged
merged 16 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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', () => {
criamico marked this conversation as resolved.
Show resolved Hide resolved
it('if agent reports not upgradeable with agent version < latest agent version', () => {
expect(getNotUpgradeableMessage(getAgent({ version: '7.9.0' }), '8.0.0')).toBe(
'agent is marked as not upgradeable in elastic-agent.'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'agent is marked as not upgradeable in elastic-agent.'
'agent cannot be upgraded through Fleet. It may be running in a container.'

cc @cmacknz - Any input on this particular message? I feel we don't want to surface something as technical as the supervisor process/PID 1 details here, but something explicit would be helpful until we get a more granular reason from #173281. Tried to borrow some wording from https://www.elastic.co/guide/en/fleet/current/elastic-agent-container.html#_what_you_need.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion would be something like "It may be running in a container or is not installed as a service".

That second scenario is the case where someone has just executed ./elastic-agent.exe directly out of the installation directory.

);
});

it('if agent reports not upgradeable with agent version > latest agent version', () => {
expect(getNotUpgradeableMessage(getAgent({ version: '8.0.0' }), '7.9.0')).toBe(
'agent is marked as not upgradeable in elastic-agent.'
criamico marked this conversation as resolved.
Show resolved Hide resolved
);
});

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 is marked as not upgradeable in elastic-agent.'
criamico marked this conversation as resolved.
Show resolved Hide resolved
);
});

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 version is already the latest.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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 version is lower than latest.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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 unenrollment was started.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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 was unenrolled.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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 version is already the latest.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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('the selected version is lower than the current version.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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 at the selected version.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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('upgrade was already started.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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('upgrade was already started.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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'
)
).toBe('the agent has been upgraded recently. Please wait.');
criamico marked this conversation as resolved.
Show resolved Hide resolved
});

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
57 changes: 57 additions & 0 deletions x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 type { Agent } from '../types';

Expand Down Expand Up @@ -42,6 +43,62 @@ 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 `agent version is missing.`;
criamico marked this conversation as resolved.
Show resolved Hide resolved
}
if (agent.unenrolled_at) {
return `agent was unenrolled.`;
}
if (agent.unenrollment_started_at) {
return `agent unenrollment was started.`;
}
if (!agent.local_metadata.elastic.agent.upgradeable) {
return `agent is marked as not upgradeable in elastic-agent.`;
}
if (isAgentUpgrading(agent)) {
return `upgrade was already started.`;
}
if (getRecentUpgradeInfoForAgent(agent).hasBeenUpgradedRecently) {
return `the agent has been upgraded recently. Please wait.`;
}
const agentVersionNumber = semverCoerce(agentVersion);
if (!agentVersionNumber) return 'agent version is not valid.';

if (versionToUpgrade !== undefined) {
const versionToUpgradeNumber = semverCoerce(versionToUpgrade);
if (!versionToUpgradeNumber) return 'the selected version is not valid.';

if (semverEq(agentVersionNumber, versionToUpgradeNumber))
return `agent is already at the selected version.`;

if (semverLt(versionToUpgradeNumber, agentVersionNumber))
return `the selected version is lower than the current version.`;
// explicitly allow this case - the agent is upgradeable
if (semverGt(versionToUpgradeNumber, agentVersionNumber)) return undefined;
}

const latestAgentVersionNumber = semverCoerce(latestAgentVersion);
if (!latestAgentVersionNumber) return 'latest version is not valid.';

if (semverEq(agentVersionNumber, latestAgentVersionNumber))
return `agent version is already the latest.`;

if (semverGt(agentVersionNumber, latestAgentVersionNumber))
return `agent version is lower than latest.`;

// 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);
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 @@ -198,4 +199,26 @@ 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 is marked as not upgradeable in elastic-agent./)
).toBeInTheDocument();
const el = utils.getByTestId('confirmModalConfirmButton');
expect(el).toBeDisabled();
});
});
});
Loading