Skip to content

Commit 44fb932

Browse files
criamicokibanamachinenchaulet
authored
[Fleet] Changes to bulk upgrade api for allowing rolling upgrades (#131947)
* [Fleet] Changes to bulk upgrade api for allowing rolling upgrades * Remove one query and some tests * Skip version check if fleet server agents are being upgraded * Fix test * Fixing tests again * Fix failing check * Fix another test * Fix another test * Fix api integration tests * Remove parameter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
1 parent 61ae132 commit 44fb932

6 files changed

Lines changed: 367 additions & 87 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { getMaxVersion } from './get_max_version';
9+
10+
describe('Fleet - getMaxVersion', () => {
11+
it('returns the maximum version', () => {
12+
const versions = ['8.1.0', '8.3.0', '8.2.1', '7.16.0', '8.2.0', '7.16.1', '8.3.1'];
13+
expect(getMaxVersion(versions)).toEqual('8.3.1');
14+
});
15+
16+
it('returns the maximum version when there are duplicates', () => {
17+
const versions = ['8.1.0', '8.3.0', '8.2.1', '7.16.0', '8.2.0', '7.16.1', '8.2.0', '7.15.1'];
18+
expect(getMaxVersion(versions)).toEqual('8.3.0');
19+
});
20+
21+
it('returns the maximum version when there is a snapshot version', () => {
22+
const versions = ['8.1.0', '8.2.0-SNAPSHOT', '7.16.0', '7.16.1'];
23+
expect(getMaxVersion(versions)).toEqual('8.2.0-SNAPSHOT');
24+
});
25+
26+
it('returns the maximum version and prefers the major version to the snapshot', () => {
27+
const versions = ['8.1.0', '8.2.0-SNAPSHOT', '8.2.0', '7.16.0', '7.16.1'];
28+
expect(getMaxVersion(versions)).toEqual('8.2.0');
29+
});
30+
31+
it('when there is only a version returns it', () => {
32+
const versions = ['8.1.0'];
33+
expect(getMaxVersion(versions)).toEqual('8.1.0');
34+
});
35+
36+
it('returns an empty string when the passed array is empty', () => {
37+
const versions: string[] = [];
38+
expect(getMaxVersion(versions)).toEqual('');
39+
});
40+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import { uniq } from 'lodash';
8+
import semverGt from 'semver/functions/gt';
9+
import semverCoerce from 'semver/functions/coerce';
10+
11+
// Find max version from an array of string versions
12+
export function getMaxVersion(versions: string[]) {
13+
const uniqVersions: string[] = uniq(versions);
14+
15+
if (uniqVersions.length === 1) {
16+
const semverVersion = semverCoerce(uniqVersions[0])?.version;
17+
return semverVersion ? semverVersion : '';
18+
} else if (uniqVersions.length > 1) {
19+
const sorted = uniqVersions.sort((a, b) => (semverGt(a, b) ? 1 : -1));
20+
return sorted[sorted.length - 1];
21+
}
22+
return '';
23+
}

x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,24 @@
77

88
import type { RequestHandler } from '@kbn/core/server';
99
import type { TypeOf } from '@kbn/config-schema';
10+
import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';
11+
1012
import semverCoerce from 'semver/functions/coerce';
13+
import semverGt from 'semver/functions/gt';
1114

1215
import type { PostAgentUpgradeResponse, PostBulkAgentUpgradeResponse } from '../../../common/types';
1316
import type { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types';
1417
import * as AgentService from '../../services/agents';
1518
import { appContextService } from '../../services';
1619
import { defaultIngestErrorHandler } from '../../errors';
20+
import { SO_SEARCH_LIMIT } from '../../../common';
1721
import { isAgentUpgradeable } from '../../../common/services';
18-
import { getAgentById } from '../../services/agents';
22+
import { getAgentById, getAgentsByKuery } from '../../services/agents';
23+
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, AGENTS_PREFIX } from '../../constants';
24+
25+
import { getMaxVersion } from '../../../common/services/get_max_version';
26+
27+
import { packagePolicyService } from '../../services/package_policy';
1928

2029
export const postAgentUpgradeHandler: RequestHandler<
2130
TypeOf<typeof PostAgentUpgradeRequestSchema.params>,
@@ -28,7 +37,7 @@ export const postAgentUpgradeHandler: RequestHandler<
2837
const { version, source_uri: sourceUri, force } = request.body;
2938
const kibanaVersion = appContextService.getKibanaVersion();
3039
try {
31-
checkVersionIsSame(version, kibanaVersion);
40+
checkKibanaVersion(version, kibanaVersion);
3241
checkSourceUriAllowed(sourceUri);
3342
} catch (err) {
3443
return response.customError({
@@ -90,8 +99,9 @@ export const postBulkAgentsUpgradeHandler: RequestHandler<
9099
} = request.body;
91100
const kibanaVersion = appContextService.getKibanaVersion();
92101
try {
93-
checkVersionIsSame(version, kibanaVersion);
102+
checkKibanaVersion(version, kibanaVersion);
94103
checkSourceUriAllowed(sourceUri);
104+
await checkFleetServerVersion(version, agents, soClient, esClient);
95105
} catch (err) {
96106
return response.customError({
97107
statusCode: 400,
@@ -125,17 +135,17 @@ export const postBulkAgentsUpgradeHandler: RequestHandler<
125135
}
126136
};
127137

128-
export const checkVersionIsSame = (version: string, kibanaVersion: string) => {
138+
export const checkKibanaVersion = (version: string, kibanaVersion: string) => {
129139
// get version number only in case "-SNAPSHOT" is in it
130140
const kibanaVersionNumber = semverCoerce(kibanaVersion)?.version;
131141
if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`);
132142
const versionToUpgradeNumber = semverCoerce(version)?.version;
133143
if (!versionToUpgradeNumber)
134144
throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`);
135-
// temporarily only allow upgrading to the same version as the installed kibana version
136-
if (kibanaVersionNumber !== versionToUpgradeNumber)
145+
146+
if (semverGt(version, kibanaVersion))
137147
throw new Error(
138-
`cannot upgrade agent to ${versionToUpgradeNumber} because it is different than the installed kibana version ${kibanaVersionNumber}`
148+
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the installed kibana version ${kibanaVersionNumber}`
139149
);
140150
};
141151

@@ -146,3 +156,67 @@ const checkSourceUriAllowed = (sourceUri?: string) => {
146156
);
147157
}
148158
};
159+
160+
// Check the installed fleet server versions
161+
// Allow upgrading if the agents to upgrade include fleet server agents
162+
const checkFleetServerVersion = async (
163+
versionToUpgradeNumber: string,
164+
agentsIds: string | string[],
165+
soClient: SavedObjectsClientContract,
166+
esClient: ElasticsearchClient
167+
) => {
168+
let packagePolicyData;
169+
try {
170+
packagePolicyData = await packagePolicyService.list(soClient, {
171+
perPage: SO_SEARCH_LIMIT,
172+
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: fleet_server`,
173+
});
174+
} catch (error) {
175+
throw new Error(error.message);
176+
}
177+
const agentPoliciesIds = packagePolicyData?.items.map((item) => item.policy_id);
178+
179+
if (agentPoliciesIds.length === 0) {
180+
return;
181+
}
182+
183+
let agentsResponse;
184+
try {
185+
agentsResponse = await getAgentsByKuery(esClient, {
186+
showInactive: false,
187+
perPage: SO_SEARCH_LIMIT,
188+
kuery: `${AGENTS_PREFIX}.policy_id:${agentPoliciesIds.map((id) => `"${id}"`).join(' or ')}`,
189+
});
190+
} catch (error) {
191+
throw new Error(error.message);
192+
}
193+
194+
const { agents: fleetServerAgents } = agentsResponse;
195+
196+
if (fleetServerAgents.length === 0) {
197+
return;
198+
}
199+
const fleetServerIds = fleetServerAgents.map((agent) => agent.id);
200+
201+
let hasFleetServerAgents: boolean;
202+
if (Array.isArray(agentsIds)) {
203+
hasFleetServerAgents = agentsIds.some((id) => fleetServerIds.includes(id));
204+
} else {
205+
hasFleetServerAgents = fleetServerIds.includes(agentsIds);
206+
}
207+
if (hasFleetServerAgents) {
208+
return;
209+
}
210+
211+
const fleetServerVersions = fleetServerAgents.map(
212+
(agent) => agent.local_metadata.elastic.agent.version
213+
) as string[];
214+
215+
const maxFleetServerVersion = getMaxVersion(fleetServerVersions);
216+
217+
if (semverGt(versionToUpgradeNumber, maxFleetServerVersion)) {
218+
throw new Error(
219+
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}`
220+
);
221+
}
222+
};

0 commit comments

Comments
 (0)