[8.13] [Fleet] Use latest compatible version in K8's manifest instead of latest available version (#179662) (#179876)

# Backport

This will backport the following commits from `main` to `8.13`:
- [[Fleet] Use latest compatible version in K8's manifest instead
of latest available version
(#179662)](https://github.com/elastic/kibana/pull/179662)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Kyle
Pollich","email":"kyle.pollich@elastic.co"},"sourceCommit":{"committedDate":"2024-04-02T21:49:46Z","message":"[Fleet]
Use latest compatible version in K8's manifest instead of latest
available version (#179662)\n\nFixes #175149 \r\n\r\n##
Summary\r\n\r\nUses the same \"latest compatible version\" logic present
in\r\n`useAgentVersion` when we render agent install instructions when
we\r\ndisplay the Docker image version for K8's manifests.\r\n\r\n## To
test\r\n\r\n1. Create a file
at\r\n`x-pack/plugins/fleet/target/agent_versions_list.json` with some
newer\r\nversions, e.g.\r\n\r\n```json\r\n[\r\n \"8.16.0\",\r\n
\"8.15.0\"\r\n]\r\n```\r\n2. Create an agent policy and add an
integration policy for the\r\n`kubernetes` integration\r\n3. Click the
`Add Agent` button in the action dropdown for your agent\r\npolicy\r\n4.
Observe the `image` value in the displayed K8's manifest has
a\r\nversion of `8.13.0` and not one of your newer
versions\r\n\r\n\r\n![image](563dd3b3-3f5f-4c4c-94e9-5fb668173d16)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"6b29552382cb84f908f02d5dc605b150d1ebbde5","branchLabelMapping":{"^v8.14.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Fleet","backport:prev-minor","ci:project-deploy-security","v8.14.0"],"title":"[Fleet]
Use latest compatible version in K8's manifest instead of latest
available
version","number":179662,"url":"https://github.com/elastic/kibana/pull/179662","mergeCommit":{"message":"[Fleet]
Use latest compatible version in K8's manifest instead of latest
available version (#179662)\n\nFixes #175149 \r\n\r\n##
Summary\r\n\r\nUses the same \"latest compatible version\" logic present
in\r\n`useAgentVersion` when we render agent install instructions when
we\r\ndisplay the Docker image version for K8's manifests.\r\n\r\n## To
test\r\n\r\n1. Create a file
at\r\n`x-pack/plugins/fleet/target/agent_versions_list.json` with some
newer\r\nversions, e.g.\r\n\r\n```json\r\n[\r\n \"8.16.0\",\r\n
\"8.15.0\"\r\n]\r\n```\r\n2. Create an agent policy and add an
integration policy for the\r\n`kubernetes` integration\r\n3. Click the
`Add Agent` button in the action dropdown for your agent\r\npolicy\r\n4.
Observe the `image` value in the displayed K8's manifest has
a\r\nversion of `8.13.0` and not one of your newer
versions\r\n\r\n\r\n![image](563dd3b3-3f5f-4c4c-94e9-5fb668173d16)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"6b29552382cb84f908f02d5dc605b150d1ebbde5"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.14.0","branchLabelMappingKey":"^v8.14.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/179662","number":179662,"mergeCommit":{"message":"[Fleet]
Use latest compatible version in K8's manifest instead of latest
available version (#179662)\n\nFixes #175149 \r\n\r\n##
Summary\r\n\r\nUses the same \"latest compatible version\" logic present
in\r\n`useAgentVersion` when we render agent install instructions when
we\r\ndisplay the Docker image version for K8's manifests.\r\n\r\n## To
test\r\n\r\n1. Create a file
at\r\n`x-pack/plugins/fleet/target/agent_versions_list.json` with some
newer\r\nversions, e.g.\r\n\r\n```json\r\n[\r\n \"8.16.0\",\r\n
\"8.15.0\"\r\n]\r\n```\r\n2. Create an agent policy and add an
integration policy for the\r\n`kubernetes` integration\r\n3. Click the
`Add Agent` button in the action dropdown for your agent\r\npolicy\r\n4.
Observe the `image` value in the displayed K8's manifest has
a\r\nversion of `8.13.0` and not one of your newer
versions\r\n\r\n\r\n![image](563dd3b3-3f5f-4c4c-94e9-5fb668173d16)\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"6b29552382cb84f908f02d5dc605b150d1ebbde5"}}]}]
BACKPORT-->

Co-authored-by: Kyle Pollich <kyle.pollich@elastic.co>
This commit is contained in:
Kibana Machine 2024-04-02 19:04:34 -04:00 committed by GitHub
parent e5027f51e5
commit 5d0cb6d742
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 142 additions and 33 deletions

View file

@ -33,7 +33,7 @@ import { getAgentById } from '../../services/agents';
import type { Agent } from '../../types';
import { getAllFleetServerAgents } from '../../collectors/get_all_fleet_server_agents';
import { getLatestAvailableVersion } from '../../services/agents/versions';
import { getLatestAvailableAgentVersion } from '../../services/agents/versions';
export const postAgentUpgradeHandler: RequestHandler<
TypeOf<typeof PostAgentUpgradeRequestSchema.params>,
@ -45,7 +45,7 @@ export const postAgentUpgradeHandler: RequestHandler<
const esClient = coreContext.elasticsearch.client.asInternalUser;
const { version, source_uri: sourceUri, force, skipRateLimitCheck } = request.body;
const kibanaVersion = appContextService.getKibanaVersion();
const latestAgentVersion = await getLatestAvailableVersion();
const latestAgentVersion = await getLatestAvailableAgentVersion();
try {
checkKibanaVersion(version, kibanaVersion, force);
} catch (err) {

View file

@ -19,7 +19,7 @@ import { HTTPAuthorizationHeader } from '../../../common/http_authorization_head
import { fullAgentPolicyToYaml } from '../../../common/services';
import { appContextService, agentPolicyService } from '../../services';
import { getAgentsByKuery } from '../../services/agents';
import { getAgentsByKuery, getLatestAvailableAgentVersion } from '../../services/agents';
import { AGENTS_PREFIX } from '../../constants';
import type {
GetAgentPoliciesRequestSchema,
@ -430,13 +430,12 @@ export const getK8sManifest: FleetRequestHandler<
undefined,
TypeOf<typeof GetK8sManifestRequestSchema.query>
> = async (context, request, response) => {
const fleetContext = await context.fleet;
try {
const fleetServer = request.query.fleetServer ?? '';
const token = request.query.enrolToken ?? '';
const agentVersion =
await fleetContext.agentClient.asInternalUser.getLatestAgentAvailableVersion();
const agentVersion = await getLatestAvailableAgentVersion();
const fullAgentManifest = await agentPolicyService.getFullAgentManifest(
fleetServer,
token,
@ -464,13 +463,10 @@ export const downloadK8sManifest: FleetRequestHandler<
undefined,
TypeOf<typeof GetK8sManifestRequestSchema.query>
> = async (context, request, response) => {
const fleetContext = await context.fleet;
try {
const fleetServer = request.query.fleetServer ?? '';
const token = request.query.enrolToken ?? '';
const agentVersion =
await fleetContext.agentClient.asInternalUser.getLatestAgentAvailableVersion();
const agentVersion = await getLatestAvailableAgentVersion();
const fullAgentManifest = await agentPolicyService.getFullAgentManifest(
fleetServer,
token,

View file

@ -26,14 +26,14 @@ import type { AgentClient } from './agent_service';
import { AgentServiceImpl } from './agent_service';
import { getAgentsByKuery, getAgentById } from './crud';
import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status';
import { getLatestAvailableVersion } from './versions';
import { getLatestAvailableAgentVersion } from './versions';
const mockGetAuthzFromRequest = getAuthzFromRequest as jest.Mock<Promise<FleetAuthz>>;
const mockGetAgentsByKuery = getAgentsByKuery as jest.Mock;
const mockGetAgentById = getAgentById as jest.Mock;
const mockGetAgentStatusById = getAgentStatusById as jest.Mock;
const mockGetAgentStatusForAgentPolicy = getAgentStatusForAgentPolicy as jest.Mock;
const mockGetLatestAvailableVersion = getLatestAvailableVersion as jest.Mock;
const mockgetLatestAvailableAgentVersion = getLatestAvailableAgentVersion as jest.Mock;
describe('AgentService', () => {
beforeEach(() => {
@ -200,11 +200,11 @@ function expectApisToCallServicesSuccessfully(
);
});
test('client.getLatestAgentAvailableVersion calls getLatestAvailableVersion and returns results', async () => {
mockGetLatestAvailableVersion.mockResolvedValue('getLatestAvailableVersion success');
test('client.getLatestAgentAvailableVersion calls getLatestAvailableAgentVersion and returns results', async () => {
mockgetLatestAvailableAgentVersion.mockResolvedValue('getLatestAvailableAgentVersion success');
await expect(agentClient.getLatestAgentAvailableVersion()).resolves.toEqual(
'getLatestAvailableVersion success'
'getLatestAvailableAgentVersion success'
);
expect(mockGetLatestAvailableVersion).toHaveBeenCalledTimes(1);
expect(mockgetLatestAvailableAgentVersion).toHaveBeenCalledTimes(1);
});
}

View file

@ -28,7 +28,7 @@ import { FleetUnauthorizedError } from '../../errors';
import { getAgentsByKuery, getAgentById } from './crud';
import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status';
import { getLatestAvailableVersion } from './versions';
import { getLatestAvailableAgentVersion } from './versions';
/**
* A service for interacting with Agent data. See {@link AgentClient} for more information.
@ -139,7 +139,7 @@ class AgentClientImpl implements AgentClient {
public async getLatestAgentAvailableVersion(includeCurrentVersion?: boolean) {
await this.#runPreflight();
return getLatestAvailableVersion(includeCurrentVersion);
return getLatestAvailableAgentVersion({ includeCurrentVersion });
}
#runPreflight = async () => {

View file

@ -33,7 +33,7 @@ jest.mock('./versions', () => {
getAvailableVersions: jest
.fn()
.mockResolvedValue(['8.4.0', '8.5.0', '8.6.0', '8.7.0', '8.8.0']),
getLatestAvailableVersion: jest.fn().mockResolvedValue('8.8.0'),
getLatestAvailableAgentVersion: jest.fn().mockResolvedValue('8.8.0'),
};
});

View file

@ -30,7 +30,7 @@ import { auditLoggingService } from '../audit_logging';
import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers';
import { buildAgentStatusRuntimeField } from './build_status_runtime_field';
import { getLatestAvailableVersion } from './versions';
import { getLatestAvailableAgentVersion } from './versions';
const INACTIVE_AGENT_CONDITION = `status:inactive OR status:unenrolled`;
const ACTIVE_AGENT_CONDITION = `NOT (${INACTIVE_AGENT_CONDITION})`;
@ -332,7 +332,7 @@ export async function getAgentsByKuery(
// filtering for a range on the version string will not work,
// nor does filtering on a flattened field (local_metadata), so filter here
if (showUpgradeable) {
const latestAgentVersion = await getLatestAvailableVersion();
const latestAgentVersion = await getLatestAvailableAgentVersion();
// fixing a bug where upgradeable filter was not returning right results https://github.com/elastic/kibana/issues/117329
// query all agents, then filter upgradeable, and return the requested page and correct total
// if there are more than SO_SEARCH_LIMIT agents, the logic falls back to same as before

View file

@ -19,4 +19,4 @@ export { getAgentUploads, getAgentUploadFile } from './uploads';
export { AgentServiceImpl } from './agent_service';
export type { AgentClient, AgentService } from './agent_service';
export { BulkActionsResolver } from './bulk_actions_resolver';
export { getAvailableVersions, getLatestAvailableVersion } from './versions';
export { getAvailableVersions, getLatestAvailableAgentVersion } from './versions';

View file

@ -20,7 +20,7 @@ jest.mock('./versions', () => {
getAvailableVersions: jest
.fn()
.mockResolvedValue(['8.4.0', '8.5.0', '8.6.0', '8.7.0', '8.8.0']),
getLatestAvailableVersion: jest.fn().mockResolvedValue('8.8.0'),
getLatestAvailableAgentVersion: jest.fn().mockResolvedValue('8.8.0'),
};
});

View file

@ -30,7 +30,7 @@ import { createErrorActionResults, createAgentAction } from './actions';
import { getHostedPolicies, isHostedAgent } from './hosted_agent';
import { BulkActionTaskType } from './bulk_action_types';
import { getCancelledActions } from './action_status';
import { getLatestAvailableVersion } from './versions';
import { getLatestAvailableAgentVersion } from './versions';
export class UpgradeActionRunner extends ActionRunner {
protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> {
@ -78,7 +78,7 @@ export async function upgradeBatch(
? givenAgents.filter((agent: Agent) => !isHostedAgent(hostedPolicies, agent))
: givenAgents;
const latestAgentVersion = await getLatestAvailableVersion();
const latestAgentVersion = await getLatestAvailableAgentVersion();
const upgradeableResults = await Promise.allSettled(
agentsToCheckUpgradeable.map(async (agent) => {
// Filter out agents that are:

View file

@ -12,7 +12,7 @@ import type { DeepPartial } from 'utility-types';
import type { FleetConfigType } from '../../../public/plugin';
import { getAvailableVersions } from './versions';
import { getAvailableVersions, getLatestAvailableAgentVersion } from './versions';
let mockKibanaVersion = '300.0.0';
let mockConfig: DeepPartial<FleetConfigType> = {};
@ -39,6 +39,91 @@ const emptyResponse = {
text: jest.fn().mockResolvedValue(JSON.stringify({})),
} as any;
beforeEach(() => {
mockedReadFile.mockReset();
mockedFetch.mockReset();
});
describe('getLatestAvailableAgentVersion', () => {
it('should return latest available version when aligned with kibana version', async () => {
mockKibanaVersion = '8.13.0';
mockedReadFile.mockResolvedValue(`["8.13.0", "8.12.2", "8.12.1", "8.12.0"]`);
mockedFetch.mockResolvedValueOnce({
status: 200,
text: jest.fn().mockResolvedValue(
JSON.stringify([
[
{
title: 'Elastic Agent 8.13.0',
version_number: '8.13.0',
},
{
title: 'Elastic Agent 8.12.2',
version_number: '8.12.2',
},
{
title: 'Elastic Agent 8.12.1',
version_number: '8.12.1',
},
{
title: 'Elastic Agent 8.12.0',
version_number: '8.12.0',
},
],
])
),
} as any);
const res = await getLatestAvailableAgentVersion({ ignoreCache: true });
expect(res).toEqual('8.13.0');
});
it('should return kibana version when kibana version is newer than latest available and API results are empty', async () => {
mockKibanaVersion = '8.14.0';
mockedReadFile.mockResolvedValue(`["8.13.0", "8.12.2", "8.12.1", "8.12.0"]`);
mockedFetch.mockResolvedValueOnce(emptyResponse);
const res = await getLatestAvailableAgentVersion({ ignoreCache: true });
expect(res).toEqual('8.14.0');
});
it('should return latest compatible version when kibana version is older than latest available', async () => {
mockKibanaVersion = '8.12.2';
mockedReadFile.mockResolvedValue(`["8.13.0", "8.12.2", "8.12.1", "8.12.0"]`);
mockedFetch.mockResolvedValueOnce({
status: 200,
text: jest.fn().mockResolvedValue(
JSON.stringify([
[
{
title: 'Elastic Agent 8.13.0',
version_number: '8.13.0',
},
{
title: 'Elastic Agent 8.12.2',
version_number: '8.12.2',
},
{
title: 'Elastic Agent 8.12.1',
version_number: '8.12.1',
},
{
title: 'Elastic Agent 8.12.0',
version_number: '8.12.0',
},
],
])
),
} as any);
const res = await getLatestAvailableAgentVersion({ ignoreCache: true });
expect(res).toEqual('8.12.2');
});
});
describe('getAvailableVersions', () => {
beforeEach(() => {
mockedReadFile.mockReset();

View file

@ -13,10 +13,14 @@ import pRetry from 'p-retry';
import { uniq } from 'lodash';
import semverGte from 'semver/functions/gte';
import semverGt from 'semver/functions/gt';
import semverRcompare from 'semver/functions/rcompare';
import semverLt from 'semver/functions/lt';
import semverCoerce from 'semver/functions/coerce';
import { REPO_ROOT } from '@kbn/repo-info';
import { differsOnlyInPatch } from '../../../common/services';
import { appContextService } from '..';
const MINIMUM_SUPPORTED_VERSION = '7.17.0';
@ -31,12 +35,36 @@ const CACHE_DURATION = 1000 * 60 * 60;
let CACHED_AVAILABLE_VERSIONS: string[] | undefined;
let LAST_FETCHED: number | undefined;
export const getLatestAvailableVersion = async (
includeCurrentVersion?: boolean
): Promise<string> => {
const versions = await getAvailableVersions({ includeCurrentVersion });
/**
* Fetch the latest available version of Elastic Agent that is compatible with the current Kibana version.
*
* e.g. if the current Kibana version is 8.12.0, and there is an 8.12.2 patch release of agent available,
* this function will return "8.12.2".
*/
export const getLatestAvailableAgentVersion = async ({
includeCurrentVersion = false,
ignoreCache = false,
}: {
includeCurrentVersion?: boolean;
ignoreCache?: boolean;
} = {}): Promise<string> => {
const kibanaVersion = appContextService.getKibanaVersion();
return versions[0];
let latestCompatibleVersion;
const versions = await getAvailableVersions({ includeCurrentVersion, ignoreCache });
versions.sort(semverRcompare);
if (versions && versions.length > 0 && versions.indexOf(kibanaVersion) !== 0) {
latestCompatibleVersion =
versions.find((version) => {
return semverLt(version, kibanaVersion) || differsOnlyInPatch(version, kibanaVersion);
}) || versions[0];
} else {
latestCompatibleVersion = kibanaVersion;
}
return latestCompatibleVersion;
};
export const getAvailableVersions = async ({

View file

@ -25,7 +25,7 @@ export interface AgentPolicyServiceInterface {
// Agent services
export { AgentServiceImpl } from './agents';
export type { AgentClient, AgentService } from './agents';
export { getAvailableVersions, getLatestAvailableVersion } from './agents';
export { getAvailableVersions, getLatestAvailableAgentVersion } from './agents';
// Saved object services
export { agentPolicyService } from './agent_policy';