[Fleet] fix UI error when entering an invalid semver (#179631)

## Summary

Closes https://github.com/elastic/kibana/issues/179592

The UI calls the semver version check functions on every keystroke, it
seems the semver functions throw error if the input is not a valid
semver (e.g. `8.14` instead of `8.14.0`). Added try-catch around the
logic which is called from the UI.

To verify:
- run a stack 8.14.0-SNAPSHOT with fleet-server
- enroll an agent with version 8.13.0
- upgrade agent, type in `8.14`
- verify that the UI error is no longer visible
- the submit button should only be enabled if typing a valid semver e.g.
`8.14.0`

<img width="1139" alt="image"
src="756e1995-f16b-4689-af19-5ffcdb57b18e">
<img width="829" alt="image"
src="1dd7ea6f-5686-4163-b367-850563abc336">


### 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
This commit is contained in:
Julia Bardi 2024-04-02 12:45:41 +02:00 committed by GitHub
parent 622380d50b
commit a1abf16b77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 120 additions and 30 deletions

View file

@ -7,6 +7,7 @@
import {
checkFleetServerVersion,
getFleetServerVersionMessage,
isAgentVersionLessThanFleetServer,
} from './check_fleet_server_versions';
@ -66,4 +67,30 @@ describe('isAgentVersionLessThanFleetServer', () => {
] as any;
expect(isAgentVersionLessThanFleetServer('8.5.0', fleetServers)).toBe(false);
});
it('should not throw if version is not a semver', () => {
const fleetServers = [
{ local_metadata: { elastic: { agent: { version: '8.13.0' } } } },
{ local_metadata: { elastic: { agent: { version: '8.14.0' } } } },
] as any;
const version = '8.14';
const result = isAgentVersionLessThanFleetServer(version, fleetServers);
expect(result).toEqual(false);
});
});
describe('getFleetServerVersionMessage', () => {
it('should not throw if version is not a semver', () => {
const fleetServers = [
{ local_metadata: { elastic: { agent: { version: '8.13.0' } } } },
{ local_metadata: { elastic: { agent: { version: '8.14.0' } } } },
] as any;
const version = '8.14';
const result = getFleetServerVersionMessage(version, fleetServers);
expect(result).toEqual('Invalid Version: 8.14');
});
});

View file

@ -41,24 +41,27 @@ export const getFleetServerVersionMessage = (
if (!maxFleetServerVersion || !versionToUpgradeNumber) {
return;
}
try {
if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
) {
return `Cannot upgrade to version ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}.`;
}
if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
) {
return `Cannot upgrade to version ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}.`;
}
const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
return `Cannot force upgrade to version ${versionToUpgradeNumber} because it does not satisfy the major and minor of the latest fleet server version ${maxFleetServerVersion}.`;
// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
return `Cannot force upgrade to version ${versionToUpgradeNumber} because it does not satisfy the major and minor of the latest fleet server version ${maxFleetServerVersion}.`;
}
} catch (e) {
return e.message;
}
};
@ -80,21 +83,25 @@ export const isAgentVersionLessThanFleetServer = (
if (!maxFleetServerVersion || !versionToUpgradeNumber) {
return false;
}
if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
)
return false;
try {
if (
!force &&
semverGt(versionToUpgradeNumber, maxFleetServerVersion) &&
!differsOnlyInPatch(versionToUpgradeNumber, maxFleetServerVersion)
)
return false;
const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
const fleetServerMajorGt =
semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
const fleetServerMajorEqMinorGte =
semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
// When force is enabled, only the major and minor versions are checked
if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
return false;
}
} catch (e) {
return false;
}

View file

@ -169,6 +169,46 @@ describe('AgentUpgradeAgentModal', () => {
});
});
it('should display invalid input if version is not a valid semver', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
id: 'agent1',
local_metadata: { host: 'abc', elastic: { agent: { version: '8.12.0' } } },
},
] as any,
agentCount: 1,
});
await waitFor(() => {
const input = utils.getByTestId('agentUpgradeModal.VersionInput');
fireEvent.input(input, { target: { value: '8.14' } });
expect(
utils.getByText('Invalid version, please use a valid semver version, e.g. 8.14.0')
).toBeInTheDocument();
});
});
it('should not display invalid input if version is a valid semver', async () => {
const { utils } = renderAgentUpgradeAgentModal({
agents: [
{
id: 'agent1',
local_metadata: { host: 'abc', elastic: { agent: { version: '8.12.0' } } },
},
] as any,
agentCount: 1,
});
await waitFor(() => {
const input = utils.getByTestId('agentUpgradeModal.VersionInput');
fireEvent.input(input, { target: { value: '8.14.0+build123456789' } });
expect(
utils.queryByText('Invalid version, please use a valid semver version, e.g. 8.14.0')
).toBeNull();
});
});
it('should display available version options', async () => {
mockSendGetAgentsAvailableVersions.mockClear();
mockSendGetAgentsAvailableVersions.mockResolvedValue({

View file

@ -28,6 +28,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui';
import semverGt from 'semver/functions/gt';
import semverLt from 'semver/functions/lt';
import semverValid from 'semver/functions/valid';
import {
AGENT_UPGRADE_COOLDOWN_IN_MIN,
@ -267,6 +268,18 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo
}
}, [agents, fleetServerAgents, isSingleAgent, latestAgentVersion, selectedVersion]);
const semverErrors = useMemo(() => {
if (!selectedVersion[0].value) return undefined;
if (!semverValid(selectedVersion[0].value)) {
return (
<FormattedMessage
id="xpack.fleet.upgradeAgents.invalidSemverError"
defaultMessage="Invalid version, please use a valid semver version, e.g. 8.14.0"
/>
);
}
}, [selectedVersion]);
const [selectedMaintenanceWindow, setSelectedMaintenanceWindow] = useState([
isSmallBatch ? maintenanceOptions[0] : maintenanceOptions[1],
]);
@ -501,13 +514,15 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo
defaultMessage: 'Upgrade version',
})}
fullWidth
isInvalid={isInvalid}
isInvalid={isInvalid || !!semverErrors}
error={
isInvalid ? (
<FormattedMessage
id="xpack.fleet.upgradeAgents.versionRequiredText"
defaultMessage="Version is required"
/>
) : !!semverErrors ? (
semverErrors
) : undefined
}
>
@ -522,6 +537,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo
setSelectedVersionStr(newValue);
setSelectedVersion([{ label: newValue, value: newValue }]);
}}
isInvalid={!!semverErrors}
/>
) : (
<EuiComboBox