[UA] Allows upgrades on cloud for minor versions (#208309)

fix https://github.com/elastic/kibana/issues/206468

## Summary

Upgrade Assistant treats upgrading to a minor or patch in the same way
as for a major and blocks the upgrade when there are critical
deprecations.
Critical deprecations only have to be resolved before upgrading to the
next major version and should not prevent upgrading to a minor or patch.

This PR refactors the blocking behavior and allows non-major upgrades
for healthy clusters.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] Cloud UI does not adapt the API to handle a query. Without query
support, calls to the API may not work as intended, or fail. Reverting
this PR will block upgrades to non major versions (next minor, next
patch) if there are critical deprecations that have not been resolved.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Christiane (Tina) Heiligers 2025-01-27 12:11:47 -07:00 committed by GitHub
parent d5764b3ee8
commit 45634ed2da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 534 additions and 118 deletions

View file

@ -11,13 +11,19 @@ Check the status of your cluster.
[[upgrade-assistant-api-status-request]]
==== Request
`GET <kibana host>:<port>/api/upgrade_assistant/status`
`GET <kibana host>:<port>/api/upgrade_assistant/status?targetVersion=9.0.0`
`targetVersion`::
(optional, string): Version to upgrade to.
[[upgrade-assistant-api-status-response-codes]]
==== Response codes
`200`::
Indicates a successful call.
`403`::
Indicates a forbidden request when the upgrade path is not supported (e.g. upgrading more than 1 major or downgrading)
[[upgrade-assistant-api-status-example]]
==== Example
@ -28,11 +34,6 @@ The API returns the following:
--------------------------------------------------
{
"readyForUpgrade": false,
"cluster": [
{
"message": "Cluster deprecated issue",
"details":"You have 2 system indices that must be migrated and 5 Elasticsearch deprecation issues and 0 Kibana deprecation issues that must be resolved before upgrading."
}
]
"details":"The following issues must be resolved before upgrading: 1 Elasticsearch deprecation issue."
}
--------------------------------------------------

View file

@ -16,7 +16,7 @@ describe('Cluster settings deprecation flyout', () => {
let testBed: ElasticsearchTestBed;
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['httpRequestsMockHelpers'];
let httpSetup: ReturnType<typeof setupEnvironment>['httpSetup'];
const clusterSettingDeprecation = esDeprecationsMockResponse.deprecations[4];
const clusterSettingDeprecation = esDeprecationsMockResponse.migrationsDeprecations[4];
beforeEach(async () => {
const mockEnvironment = setupEnvironment();

View file

@ -46,7 +46,7 @@ describe('Default deprecation flyout', () => {
});
test('renders a flyout with deprecation details', async () => {
const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2];
const multiFieldsDeprecation = esDeprecationsMockResponse.migrationsDeprecations[2];
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('default', 0);

View file

@ -60,7 +60,7 @@ describe('ES deprecations table', () => {
// Verify all deprecations appear in the table
expect(find('deprecationTableRow').length).toEqual(
esDeprecationsMockResponse.deprecations.length
esDeprecationsMockResponse.migrationsDeprecations.length
);
});
@ -69,8 +69,8 @@ describe('ES deprecations table', () => {
await actions.table.clickRefreshButton();
const mlDeprecation = esDeprecationsMockResponse.deprecations[0];
const reindexDeprecation = esDeprecationsMockResponse.deprecations[3];
const mlDeprecation = esDeprecationsMockResponse.migrationsDeprecations[0];
const reindexDeprecation = esDeprecationsMockResponse.migrationsDeprecations[3];
// Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 4 requests made
expect(httpSetup.get).toHaveBeenCalledWith(
@ -96,10 +96,10 @@ describe('ES deprecations table', () => {
it('shows critical and warning deprecations count', () => {
const { find } = testBed;
const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter(
const criticalDeprecations = esDeprecationsMockResponse.migrationsDeprecations.filter(
(deprecation) => deprecation.isCritical
);
const warningDeprecations = esDeprecationsMockResponse.deprecations.filter(
const warningDeprecations = esDeprecationsMockResponse.migrationsDeprecations.filter(
(deprecation) => deprecation.isCritical === false
);
@ -133,7 +133,7 @@ describe('ES deprecations table', () => {
await actions.searchBar.clickCriticalFilterButton();
const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter(
const criticalDeprecations = esDeprecationsMockResponse.migrationsDeprecations.filter(
(deprecation) => deprecation.isCritical
);
@ -142,7 +142,7 @@ describe('ES deprecations table', () => {
await actions.searchBar.clickCriticalFilterButton();
expect(find('deprecationTableRow').length).toEqual(
esDeprecationsMockResponse.deprecations.length
esDeprecationsMockResponse.migrationsDeprecations.length
);
});
@ -165,7 +165,7 @@ describe('ES deprecations table', () => {
component.update();
const clusterDeprecations = esDeprecationsMockResponse.deprecations.filter(
const clusterDeprecations = esDeprecationsMockResponse.migrationsDeprecations.filter(
(deprecation) => deprecation.type === 'cluster_settings'
);
@ -174,7 +174,7 @@ describe('ES deprecations table', () => {
it('filters results by query string', async () => {
const { find, actions } = testBed;
const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2];
const multiFieldsDeprecation = esDeprecationsMockResponse.migrationsDeprecations[2];
await actions.searchBar.setSearchInputValue(multiFieldsDeprecation.message);
@ -205,7 +205,7 @@ describe('ES deprecations table', () => {
describe('pagination', () => {
const esDeprecationsMockResponseWithManyDeprecations = createEsDeprecationsMockResponse(20);
const { deprecations } = esDeprecationsMockResponseWithManyDeprecations;
const { migrationsDeprecations } = esDeprecationsMockResponseWithManyDeprecations;
beforeEach(async () => {
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(
@ -229,7 +229,7 @@ describe('ES deprecations table', () => {
const { find, actions } = testBed;
expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(
Math.round(deprecations.length / 50) // Default rows per page is 50
Math.round(migrationsDeprecations.length / 50) // Default rows per page is 50
);
expect(find('deprecationTableRow').length).toEqual(50);
@ -237,7 +237,7 @@ describe('ES deprecations table', () => {
await actions.pagination.clickPaginationAt(1);
// On the second (last) page, we expect to see the remaining deprecations
expect(find('deprecationTableRow').length).toEqual(deprecations.length - 50);
expect(find('deprecationTableRow').length).toEqual(migrationsDeprecations.length - 50);
});
it('allows the number of viewable rows to change', async () => {
@ -260,15 +260,17 @@ describe('ES deprecations table', () => {
component.update();
expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(
Math.round(deprecations.length / 100) // Rows per page is now 100
Math.round(migrationsDeprecations.length / 100) // Rows per page is now 100
);
expect(find('deprecationTableRow').length).toEqual(deprecations.length);
expect(find('deprecationTableRow').length).toEqual(migrationsDeprecations.length);
});
it('updates pagination when filters change', async () => {
const { actions, find } = testBed;
const criticalDeprecations = deprecations.filter((deprecation) => deprecation.isCritical);
const criticalDeprecations = migrationsDeprecations.filter(
(deprecation) => deprecation.isCritical
);
await actions.searchBar.clickCriticalFilterButton();
@ -279,7 +281,7 @@ describe('ES deprecations table', () => {
it('updates pagination on search', async () => {
const { actions, find } = testBed;
const reindexDeprecations = deprecations.filter(
const reindexDeprecations = migrationsDeprecations.filter(
(deprecation) => deprecation.correctiveAction?.type === 'reindex'
);
@ -295,7 +297,9 @@ describe('ES deprecations table', () => {
beforeEach(async () => {
const noDeprecationsResponse = {
totalCriticalDeprecations: 0,
deprecations: [],
migrationsDeprecations: [],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(noDeprecationsResponse);

View file

@ -20,7 +20,7 @@ describe('Index settings deprecation flyout', () => {
let testBed: ElasticsearchTestBed;
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['httpRequestsMockHelpers'];
let httpSetup: ReturnType<typeof setupEnvironment>['httpSetup'];
const indexSettingDeprecation = esDeprecationsMockResponse.deprecations[1];
const indexSettingDeprecation = esDeprecationsMockResponse.migrationsDeprecations[1];
beforeEach(async () => {
const mockEnvironment = setupEnvironment();
httpRequestsMockHelpers = mockEnvironment.httpRequestsMockHelpers;

View file

@ -14,7 +14,7 @@ import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './moc
describe('Machine learning deprecation flyout', () => {
let testBed: ElasticsearchTestBed;
const mlDeprecation = esDeprecationsMockResponse.deprecations[0];
const mlDeprecation = esDeprecationsMockResponse.migrationsDeprecations[0];
let httpRequestsMockHelpers: ReturnType<typeof setupEnvironment>['httpRequestsMockHelpers'];
let httpSetup: ReturnType<typeof setupEnvironment>['httpSetup'];
beforeEach(async () => {

View file

@ -77,13 +77,15 @@ const MOCK_DEFAULT_DEPRECATION: EnrichedDeprecationInfo = {
export const esDeprecationsMockResponse: ESUpgradeStatus = {
totalCriticalDeprecations: 2,
deprecations: [
migrationsDeprecations: [
MOCK_ML_DEPRECATION,
MOCK_INDEX_SETTING_DEPRECATION,
MOCK_DEFAULT_DEPRECATION,
MOCK_REINDEX_DEPRECATION,
MOCK_CLUSTER_SETTING_DEPRECATION,
],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
// Useful for testing pagination where a large number of deprecations are needed
@ -118,7 +120,7 @@ export const createEsDeprecationsMockResponse = (
() => MOCK_DEFAULT_DEPRECATION
);
const deprecations: EnrichedDeprecationInfo[] = [
const migrationsDeprecations: EnrichedDeprecationInfo[] = [
...defaultDeprecations,
...reindexDeprecations,
...indexSettingsDeprecations,
@ -127,6 +129,8 @@ export const createEsDeprecationsMockResponse = (
return {
totalCriticalDeprecations: mlDeprecations.length + reindexDeprecations.length,
deprecations,
migrationsDeprecations,
totalCriticalHealthIssues: esDeprecationsMockResponse.totalCriticalHealthIssues,
enrichedHealthIndicators: esDeprecationsMockResponse.enrichedHealthIndicators,
};
};

View file

@ -68,7 +68,7 @@ describe('Reindex deprecation flyout', () => {
});
it('renders a flyout with reindexing details', async () => {
const reindexDeprecation = esDeprecationsMockResponse.deprecations[3];
const reindexDeprecation = esDeprecationsMockResponse.migrationsDeprecations[3];
const { actions, find, exists } = testBed;
await actions.table.clickDeprecationRowAt('reindex', 0);

View file

@ -9,7 +9,7 @@ import { ESUpgradeStatus } from '../../../../common/types';
export const esCriticalAndWarningDeprecations: ESUpgradeStatus = {
totalCriticalDeprecations: 1,
deprecations: [
migrationsDeprecations: [
{
isCritical: true,
type: 'cluster_settings',
@ -34,11 +34,13 @@ export const esCriticalAndWarningDeprecations: ESUpgradeStatus = {
},
},
],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
export const esCriticalOnlyDeprecations: ESUpgradeStatus = {
totalCriticalDeprecations: 1,
deprecations: [
migrationsDeprecations: [
{
isCritical: true,
type: 'cluster_settings',
@ -49,9 +51,13 @@ export const esCriticalOnlyDeprecations: ESUpgradeStatus = {
'The Index Lifecycle Management poll interval setting [indices.lifecycle.poll_interval] is currently set to [500ms], but must be 1s or greater',
},
],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
export const esNoDeprecations: ESUpgradeStatus = {
totalCriticalDeprecations: 0,
deprecations: [],
migrationsDeprecations: [],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};

View file

@ -244,7 +244,9 @@ export interface CloudBackupStatus {
export interface ESUpgradeStatus {
totalCriticalDeprecations: number;
deprecations: EnrichedDeprecationInfo[];
migrationsDeprecations: EnrichedDeprecationInfo[];
totalCriticalHealthIssues: number;
enrichedHealthIndicators: EnrichedDeprecationInfo[];
}
export interface ResolveIndexResponseFromES {

View file

@ -127,8 +127,8 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
warningDeprecations: number;
criticalDeprecations: number;
} = useMemo(
() => getDeprecationCountByLevel(esDeprecations?.deprecations || []),
[esDeprecations?.deprecations]
() => getDeprecationCountByLevel(esDeprecations?.migrationsDeprecations || []),
[esDeprecations?.migrationsDeprecations]
);
useEffect(() => {
@ -152,7 +152,7 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
return <SectionLoading>{i18nTexts.isLoading}</SectionLoading>;
}
if (esDeprecations?.deprecations?.length === 0) {
if (esDeprecations?.migrationsDeprecations?.length === 0) {
return (
<NoDeprecationsPrompt
deprecationType="Elasticsearch"
@ -198,7 +198,10 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => {
<EuiSpacer size="l" />
<EsDeprecationsTable deprecations={esDeprecations?.deprecations} reload={resendRequest} />
<EsDeprecationsTable
deprecations={esDeprecations?.migrationsDeprecations}
reload={resendRequest}
/>
</div>
);
});

View file

@ -23,11 +23,13 @@ export const EsDeprecationIssuesPanel: FunctionComponent<Props> = ({ setIsFixed
const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations();
const criticalDeprecationsCount =
esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical)?.length ?? 0;
esDeprecations?.migrationsDeprecations?.filter((deprecation) => deprecation.isCritical)
?.length ?? 0;
const warningDeprecationsCount =
esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false)
?.length ?? 0;
esDeprecations?.migrationsDeprecations?.filter(
(deprecation) => deprecation.isCritical === false
)?.length ?? 0;
const errorMessage = error && getEsDeprecationError(error).message;

View file

@ -2,7 +2,8 @@
exports[`getESUpgradeStatus returns the correct shape of data 1`] = `
Object {
"deprecations": Array [
"enrichedHealthIndicators": Array [],
"migrationsDeprecations": Array [
Object {
"correctiveAction": undefined,
"details": "templates using \`template\` field: security_audit_log,watches,.monitoring-alerts,triggered_watches,.ml-anomalies-,.ml-notifications,.ml-meta,.monitoring-kibana,.monitoring-es,.monitoring-logstash,.watch-history-6,.ml-state,security-index-template",
@ -183,5 +184,6 @@ Object {
},
],
"totalCriticalDeprecations": 6,
"totalCriticalHealthIssues": 0,
}
`;

View file

@ -126,9 +126,15 @@ describe('getESUpgradeStatus', () => {
});
const upgradeStatus = await getESUpgradeStatus(esClient, featureSet);
expect(upgradeStatus.deprecations).toHaveLength(0);
expect(upgradeStatus.totalCriticalDeprecations).toBe(0);
const {
totalCriticalDeprecations,
migrationsDeprecations,
totalCriticalHealthIssues,
enrichedHealthIndicators,
} = upgradeStatus;
expect([...migrationsDeprecations, ...enrichedHealthIndicators]).toHaveLength(0);
expect(totalCriticalDeprecations).toBe(0);
expect(totalCriticalHealthIssues).toBe(0);
});
it('filters out ml_settings if featureSet.mlSnapshots is set to false', async () => {
@ -140,7 +146,10 @@ describe('getESUpgradeStatus', () => {
esClient.asCurrentUser.migration.deprecations.mockResponse(mockResponse);
const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { ...featureSet });
expect(enabledUpgradeStatus.deprecations).toHaveLength(2);
expect([
...enabledUpgradeStatus.migrationsDeprecations,
...enabledUpgradeStatus.enrichedHealthIndicators,
]).toHaveLength(2);
expect(enabledUpgradeStatus.totalCriticalDeprecations).toBe(1);
const disabledUpgradeStatus = await getESUpgradeStatus(esClient, {
@ -148,7 +157,10 @@ describe('getESUpgradeStatus', () => {
mlSnapshots: false,
});
expect(disabledUpgradeStatus.deprecations).toHaveLength(0);
expect([
...disabledUpgradeStatus.migrationsDeprecations,
...disabledUpgradeStatus.enrichedHealthIndicators,
]).toHaveLength(0);
expect(disabledUpgradeStatus.totalCriticalDeprecations).toBe(0);
});
@ -160,7 +172,10 @@ describe('getESUpgradeStatus', () => {
esClient.asCurrentUser.migration.deprecations.mockResponse(mockResponse);
const enabledUpgradeStatus = await getESUpgradeStatus(esClient, { ...featureSet });
expect(enabledUpgradeStatus.deprecations).toHaveLength(1);
expect([
...enabledUpgradeStatus.migrationsDeprecations,
...enabledUpgradeStatus.enrichedHealthIndicators,
]).toHaveLength(1);
expect(enabledUpgradeStatus.totalCriticalDeprecations).toBe(1);
const disabledUpgradeStatus = await getESUpgradeStatus(esClient, {
@ -168,7 +183,10 @@ describe('getESUpgradeStatus', () => {
migrateDataStreams: false,
});
expect(disabledUpgradeStatus.deprecations).toHaveLength(0);
expect([
...disabledUpgradeStatus.migrationsDeprecations,
...disabledUpgradeStatus.enrichedHealthIndicators,
]).toHaveLength(0);
expect(disabledUpgradeStatus.totalCriticalDeprecations).toBe(0);
});
@ -203,7 +221,10 @@ describe('getESUpgradeStatus', () => {
reindexCorrectiveActions: false,
});
expect(upgradeStatus.deprecations).toHaveLength(0);
expect([
...upgradeStatus.migrationsDeprecations,
...upgradeStatus.enrichedHealthIndicators,
]).toHaveLength(0);
expect(upgradeStatus.totalCriticalDeprecations).toBe(0);
});
@ -235,9 +256,11 @@ describe('getESUpgradeStatus', () => {
});
const upgradeStatus = await getESUpgradeStatus(esClient, featureSet);
expect(upgradeStatus.totalCriticalDeprecations).toBe(2);
expect(upgradeStatus.deprecations).toMatchInlineSnapshot(`
expect(upgradeStatus.totalCriticalHealthIssues + upgradeStatus.totalCriticalDeprecations).toBe(
2
);
expect([...upgradeStatus.enrichedHealthIndicators, ...upgradeStatus.migrationsDeprecations])
.toMatchInlineSnapshot(`
Array [
Object {
"correctiveAction": Object {

View file

@ -56,14 +56,22 @@ export async function getESUpgradeStatus(
return status !== 'green';
}) as EnrichedDeprecationInfo[];
return [...enrichedHealthIndicators, ...toggledMigrationsDeprecations];
return {
enrichedHealthIndicators,
migrationsDeprecations: toggledMigrationsDeprecations,
};
};
const { enrichedHealthIndicators, migrationsDeprecations } = await getCombinedDeprecations();
const combinedDeprecations = await getCombinedDeprecations();
const criticalWarnings = combinedDeprecations.filter(({ isCritical }) => isCritical === true);
return {
totalCriticalDeprecations: criticalWarnings.length,
deprecations: combinedDeprecations,
const result = {
totalCriticalDeprecations: migrationsDeprecations.filter(
({ isCritical }) => isCritical === true
).length,
migrationsDeprecations,
totalCriticalHealthIssues: enrichedHealthIndicators.filter(
({ isCritical }) => isCritical === true
).length,
enrichedHealthIndicators,
};
return result;
}

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getUpgradeType } from './upgrade_type';
import { versionService } from './version';
import semver, { SemVer } from 'semver';
describe('getUpgradeType', () => {
let current: SemVer;
beforeEach(() => {
versionService.setup('8.0.0');
current = versionService.getCurrentVersion();
});
it('returns null if the upgrade target version is the same as the current version', () => {
const target = current.raw.toString();
const result = getUpgradeType({ current, target });
expect(result).toBeNull();
});
it("returns 'major' if the upgrade target version is the next major", () => {
const target = semver.inc(current, 'major')?.toString()!;
const result = getUpgradeType({ current, target });
expect(result).toBe('major');
});
it("returns 'minor' if the upgrade target version is the next minor", () => {
const target = semver.inc(current, 'minor')?.toString()!;
const result = getUpgradeType({ current, target });
expect(result).toBe('minor');
});
it('returns undefined if the upgrade target version is more than 1 major', () => {
const target = new SemVer('10.0.0').raw.toString();
const result = getUpgradeType({ current, target });
expect(result).toBeUndefined();
});
it('returns undefined if the upgrade target version is less than the current version', () => {
const target = new SemVer('7.0.0').raw.toString();
const result = getUpgradeType({ current, target });
expect(result).toBeUndefined();
});
});

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import semver, { SemVer } from 'semver';
export interface UpgradeTypeParams {
current: SemVer;
target: string;
}
/**
* @param {SemVer} current kibana version
* @param {string} target version to upgrade to, defaults to next major
* @returns {semver.ReleaseType | null | undefined} null if same version, undefined if target version is out of bounds
*/
export const getUpgradeType = ({ current, target }: UpgradeTypeParams) => {
const targetVersion = semver.coerce(target)!;
const versionDiff = targetVersion.major - current.major;
if (versionDiff > 1 || versionDiff < 0) {
return;
}
return semver.diff(current, semver.coerce(target)!);
};

View file

@ -7,7 +7,15 @@
import SemVer from 'semver/classes/semver';
export class Version {
export interface IVersion {
setup(version: string): void;
getCurrentVersion(): SemVer;
getMajorVersion(): number;
getNextMajorVersion(): number;
getPrevMajorVersion(): number;
}
export class Version implements IVersion {
private version!: SemVer;
public setup(version: string) {

View file

@ -119,6 +119,8 @@ export class UpgradeAssistantServerPlugin implements Plugin {
});
const router = http.createRouter();
// Initialize version service with current kibana version
versionService.setup(this.kibanaVersion);
const dependencies: RouteDependencies = {
router,
@ -139,11 +141,10 @@ export class UpgradeAssistantServerPlugin implements Plugin {
featureSet: this.initialFeatureSet,
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
},
current: versionService.getCurrentVersion(),
defaultTarget: versionService.getNextMajorVersion(),
};
// Initialize version service with current kibana version
versionService.setup(this.kibanaVersion);
registerRoutes(dependencies, this.getWorker.bind(this));
if (usageCollection) {

View file

@ -54,8 +54,10 @@ describe('ES deprecations API', () => {
describe('GET /api/upgrade_assistant/es_deprecations', () => {
it('returns state', async () => {
ESUpgradeStatusApis.getESUpgradeStatus.mockResolvedValue({
deprecations: [],
migrationsDeprecations: [],
enrichedHealthIndicators: [],
totalCriticalDeprecations: 0,
totalCriticalHealthIssues: 0,
});
const resp = await routeDependencies.router.getHandler({
method: 'get',
@ -64,7 +66,7 @@ describe('ES deprecations API', () => {
expect(resp.status).toEqual(200);
expect(JSON.stringify(resp.payload)).toMatchInlineSnapshot(
`"{\\"deprecations\\":[],\\"totalCriticalDeprecations\\":0}"`
`"{\\"migrationsDeprecations\\":[],\\"enrichedHealthIndicators\\":[],\\"totalCriticalDeprecations\\":0,\\"totalCriticalHealthIssues\\":0}"`
);
});

View file

@ -41,7 +41,7 @@ export function registerESDeprecationRoutes({
const asCurrentUser = client.asCurrentUser;
const reindexActions = reindexActionsFactory(savedObjectsClient, asCurrentUser);
const reindexService = reindexServiceFactory(asCurrentUser, reindexActions, log, licensing);
const indexNames = status.deprecations
const indexNames = [...status.migrationsDeprecations, ...status.enrichedHealthIndicators]
.filter(({ index }) => typeof index !== 'undefined')
.map(({ index }) => index as string);

View file

@ -15,7 +15,10 @@ import { getESUpgradeStatus } from '../lib/es_deprecations_status';
import { getKibanaUpgradeStatus } from '../lib/kibana_status';
import { getESSystemIndicesMigrationStatus } from '../lib/es_system_indices_migration';
import type { FeatureSet } from '../../common/types';
import { versionService } from '../lib/version';
import { getMockVersionInfo } from '../lib/__fixtures__/version';
const { currentVersion, nextMajor } = getMockVersionInfo();
jest.mock('../lib/es_version_precheck', () => ({
versionCheckHandlerWrapper: (a: any) => a,
}));
@ -36,28 +39,36 @@ jest.mock('../lib/es_system_indices_migration', () => ({
const getESSystemIndicesMigrationStatusMock = getESSystemIndicesMigrationStatus as jest.Mock;
const esDeprecationsResponse = {
cluster: [
totalCriticalDeprecations: 1,
migrationsDeprecations: [
{
level: 'critical',
message:
'Model snapshot [1] for job [deprecation_check_job] has an obsolete minimum version [6.3.0].',
details: 'Delete model snapshot [1] or update it to 7.0.0 or greater.',
url: 'doc_url',
correctiveAction: {
type: 'mlSnapshot',
snapshotId: '1',
jobId: 'deprecation_check_job',
},
// This is a critical migration deprecation object, but it's not used in the tests
},
],
indices: [],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
const esHealthResponse = {
totalCriticalDeprecations: 1,
migrationsDeprecations: [
{
// This is a critical migration deprecation object, but it's not used in the tests
},
],
totalCriticalHealthIssues: 1,
enrichedHealthIndicators: [
{
status: 'red', // this is a critical health issue
},
],
};
const esNoDeprecationsResponse = {
cluster: [],
indices: [],
totalCriticalDeprecations: 0,
migrationsDeprecations: [],
totalCriticalHealthIssues: 0,
enrichedHealthIndicators: [],
};
const systemIndicesMigrationResponse = {
@ -87,27 +98,32 @@ const systemIndicesNoMigrationResponse = {
};
describe('Status API', () => {
const registerRoutes = (featureSetOverrides: Partial<FeatureSet> = {}) => {
const mockRouter = createMockRouter();
const routeDependencies: any = {
config: {
featureSet: {
mlSnapshots: true,
migrateSystemIndices: true,
reindexCorrectiveActions: true,
...featureSetOverrides,
beforeAll(() => {
versionService.setup('8.17.0');
});
describe('GET /api/upgrade_assistant/status for major upgrade', () => {
const registerRoutes = (featureSetOverrides: Partial<FeatureSet> = {}) => {
const mockRouter = createMockRouter();
const routeDependencies: any = {
config: {
featureSet: {
mlSnapshots: true,
migrateSystemIndices: true,
reindexCorrectiveActions: true,
...featureSetOverrides,
},
},
},
router: mockRouter,
lib: { handleEsError },
router: mockRouter,
lib: { handleEsError },
current: currentVersion,
defaultTarget: nextMajor,
};
registerUpgradeStatusRoute(routeDependencies);
return { mockRouter, routeDependencies };
};
registerUpgradeStatusRoute(routeDependencies);
return { mockRouter, routeDependencies };
};
describe('GET /api/upgrade_assistant/status', () => {
afterEach(() => {
jest.resetAllMocks();
});
@ -128,6 +144,7 @@ describe('Status API', () => {
})(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(1);
expect(getKibanaUpgradeStatusMock).toBeCalledTimes(1);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: false,
@ -244,4 +261,187 @@ describe('Status API', () => {
).rejects.toThrow('test error');
});
});
describe('GET /api/upgrade_assistant/status for non-major upgrade', () => {
const registerRoutes = (featureSetOverrides: Partial<FeatureSet> = {}) => {
const mockRouter = createMockRouter();
const routeDependencies: any = {
config: {
featureSet: {
mlSnapshots: true,
migrateSystemIndices: true,
reindexCorrectiveActions: true,
...featureSetOverrides,
},
},
router: mockRouter,
lib: { handleEsError },
current: currentVersion,
defaultTarget: nextMajor,
};
registerUpgradeStatusRoute(routeDependencies);
return { mockRouter, routeDependencies };
};
const testQuery = { query: { targetVersion: '8.18.0' } };
afterEach(() => {
jest.resetAllMocks();
});
it('returns readyForUpgrade === false if ES contains critical health issues, ignoring deprecations', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockResolvedValue(esHealthResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 1,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesNoMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(1);
expect(getKibanaUpgradeStatusMock).toBeCalledTimes(1);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: false,
details:
'The following issues must be resolved before upgrading: 1 Elasticsearch deprecation issue.',
});
});
it('returns readyForUpgrade === true if Kibana or ES contain critical deprecations and no system indices need migration', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockResolvedValue(esDeprecationsResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 1,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesNoMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(1);
expect(getKibanaUpgradeStatusMock).toBeCalledTimes(1);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: true,
details: 'All deprecation warnings have been resolved.',
});
});
it('returns readyForUpgrade === true if Kibana or ES contain critical deprecations and system indices need migration', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockResolvedValue(esDeprecationsResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 1,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(1);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: true,
details: 'All deprecation warnings have been resolved.',
});
});
it('returns readyForUpgrade === true if no critical Kibana or ES deprecations but system indices need migration', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockResolvedValue(esNoDeprecationsResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 0,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(1);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: true,
details: 'All deprecation warnings have been resolved.',
});
});
it('returns readyForUpgrade === true if there are no critical deprecations and no system indices need migration', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockResolvedValue(esNoDeprecationsResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 0,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesNoMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: true,
details: 'All deprecation warnings have been resolved.',
});
});
it('skips ES system indices migration check when featureSet.migrateSystemIndices is set to false', async () => {
const { routeDependencies } = registerRoutes({ migrateSystemIndices: false });
getESUpgradeStatusMock.mockResolvedValue(esNoDeprecationsResponse);
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 0,
});
getESSystemIndicesMigrationStatusMock.mockResolvedValue(systemIndicesMigrationResponse);
const resp = await routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory);
expect(getESSystemIndicesMigrationStatusMock).toBeCalledTimes(0);
expect(resp.status).toEqual(200);
expect(resp.payload).toEqual({
readyForUpgrade: true,
details: 'All deprecation warnings have been resolved.',
});
});
it('returns an error if it throws', async () => {
const { routeDependencies } = registerRoutes();
getESUpgradeStatusMock.mockRejectedValue(new Error('test error'));
getKibanaUpgradeStatusMock.mockResolvedValue({
totalCriticalDeprecations: 0,
});
await expect(
routeDependencies.router.getHandler({
method: 'get',
pathPattern: '/api/upgrade_assistant/status',
})(routeHandlerContextMock, createRequestMock(testQuery), kibanaResponseFactory)
).rejects.toThrow('test error');
});
});
});

View file

@ -4,14 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../common/constants';
import { getESUpgradeStatus } from '../lib/es_deprecations_status';
import { versionCheckHandlerWrapper } from '../lib/es_version_precheck';
import { getKibanaUpgradeStatus } from '../lib/kibana_status';
import { getESSystemIndicesMigrationStatus } from '../lib/es_system_indices_migration';
import { RouteDependencies } from '../types';
import { getUpgradeType } from '../lib/upgrade_type';
/**
* Note that this route is primarily intended for consumption by Cloud.
@ -20,6 +21,8 @@ export function registerUpgradeStatusRoute({
config: { featureSet },
router,
lib: { handleEsError },
current,
defaultTarget,
}: RouteDependencies) {
router.get(
{
@ -34,25 +37,33 @@ export function registerUpgradeStatusRoute({
access: 'public',
summary: `Get upgrade readiness status`,
},
validate: false,
validate: {
query: schema.object({
targetVersion: schema.maybe(schema.string()),
}),
},
},
versionCheckHandlerWrapper(async ({ core }, request, response) => {
const targetVersion = request.query?.targetVersion || `${defaultTarget}`;
const upgradeType = getUpgradeType({ current, target: targetVersion });
if (!upgradeType) return response.forbidden();
try {
const {
elasticsearch: { client: esClient },
deprecations: { client: deprecationsClient },
} = await core;
// Fetch ES upgrade status
const { totalCriticalDeprecations: esTotalCriticalDeps } = await getESUpgradeStatus(
esClient,
featureSet
);
const {
totalCriticalDeprecations, // critical deprecations
totalCriticalHealthIssues, // critical health issues
} = await getESUpgradeStatus(esClient, featureSet);
const getSystemIndicesMigrationStatus = async () => {
/**
* Skip system indices migration status check if `featureSet.migrateSystemIndices`
* is set to `false`. This flag is enabled from configs for major version stack ugprades.
* returns `migration_status: 'NO_MIGRATION_NEEDED'` to indicate no migation needed.
* returns `migration_status: 'NO_MIGRATION_NEEDED'` to indicate no migration needed.
*/
if (!featureSet.migrateSystemIndices) {
return {
@ -76,10 +87,19 @@ export function registerUpgradeStatusRoute({
const { totalCriticalDeprecations: kibanaTotalCriticalDeps } = await getKibanaUpgradeStatus(
deprecationsClient
);
const readyForUpgrade =
esTotalCriticalDeps === 0 &&
kibanaTotalCriticalDeps === 0 &&
systemIndicesMigrationStatus === 'NO_MIGRATION_NEEDED';
// non-major upgrades blocked only for health issues (status !== green)
let upgradeTypeBasedReadyForUpgrade: boolean;
if (upgradeType === 'major') {
upgradeTypeBasedReadyForUpgrade =
totalCriticalHealthIssues === 0 &&
totalCriticalDeprecations === 0 &&
kibanaTotalCriticalDeps === 0 &&
systemIndicesMigrationStatus === 'NO_MIGRATION_NEEDED';
} else {
upgradeTypeBasedReadyForUpgrade = totalCriticalHealthIssues === 0;
}
const readyForUpgrade = upgradeType && upgradeTypeBasedReadyForUpgrade;
const getStatusMessage = () => {
if (readyForUpgrade) {
@ -89,8 +109,12 @@ export function registerUpgradeStatusRoute({
}
const upgradeIssues: string[] = [];
let esTotalCriticalDeps = totalCriticalHealthIssues;
if (upgradeType === 'major') {
esTotalCriticalDeps += totalCriticalDeprecations;
}
if (notMigratedSystemIndices) {
if (upgradeType === 'major' && notMigratedSystemIndices) {
upgradeIssues.push(
i18n.translate('xpack.upgradeAssistant.status.systemIndicesMessage', {
defaultMessage:
@ -99,7 +123,7 @@ export function registerUpgradeStatusRoute({
})
);
}
// can be improved by showing health indicator issues separately
if (esTotalCriticalDeps) {
upgradeIssues.push(
i18n.translate('xpack.upgradeAssistant.status.esTotalCriticalDepsMessage', {
@ -110,7 +134,7 @@ export function registerUpgradeStatusRoute({
);
}
if (kibanaTotalCriticalDeps) {
if (upgradeType === 'major' && kibanaTotalCriticalDeps) {
upgradeIssues.push(
i18n.translate('xpack.upgradeAssistant.status.kibanaTotalCriticalDepsMessage', {
defaultMessage:
@ -119,7 +143,6 @@ export function registerUpgradeStatusRoute({
})
);
}
return i18n.translate('xpack.upgradeAssistant.status.deprecationsUnresolvedMessage', {
defaultMessage:
'The following issues must be resolved before upgrading: {upgradeIssues}.',

View file

@ -8,6 +8,7 @@
import { IRouter, Logger, SavedObjectsServiceStart } from '@kbn/core/server';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/server';
import { SecurityPluginStart } from '@kbn/security-plugin/server';
import SemVer from 'semver/classes/semver';
import { CredentialStore } from './lib/reindexing/credential_store';
import { handleEsError } from './shared_imports';
import type { FeatureSet } from '../common/types';
@ -26,4 +27,6 @@ export interface RouteDependencies {
featureSet: FeatureSet;
isSecurityEnabled: () => boolean;
};
current: SemVer;
defaultTarget: number;
}

View file

@ -128,13 +128,68 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
const expectedResponseKeys = ['readyForUpgrade', 'details'];
// We're not able to easily test different upgrade status scenarios (there are tests with mocked data to handle this)
// so, for now, we simply verify the response returns the expected format
expectedResponseKeys.forEach((key) => {
expect(body[key]).to.not.equal(undefined);
});
});
it('returns a successful response when upgrading to the next minor', async () => {
const { body } = await supertest
.get('/api/upgrade_assistant/status')
.query({
targetVersion: '9.1.0',
})
.set('kbn-xsrf', 'xxx')
.expect(200);
const expectedResponseKeys = ['readyForUpgrade', 'details'];
// We're not able to easily test different upgrade status scenarios (there are tests with mocked data to handle this)
// so, for now, we simply verify the response returns the expected format
expectedResponseKeys.forEach((key) => {
expect(body[key]).to.not.equal(undefined);
});
});
it('returns a successful response when upgrading to the next major', async () => {
const { body } = await supertest
.get('/api/upgrade_assistant/status')
.query({
targetVersion: '10.0.0',
})
.set('kbn-xsrf', 'xxx')
.expect(200);
const expectedResponseKeys = ['readyForUpgrade', 'details'];
// We're not able to easily test different upgrade status scenarios (there are tests with mocked data to handle this)
// so, for now, we simply verify the response returns the expected format
expectedResponseKeys.forEach((key) => {
expect(body[key]).to.not.equal(undefined);
});
});
it('returns 403 forbidden error when upgrading more than 1 major', async () => {
const { body } = await supertest
.get('/api/upgrade_assistant/status')
.query({
targetVersion: '11.0.0',
})
.set('kbn-xsrf', 'xxx')
.expect(403);
expect(body.message).to.be('Forbidden');
});
it('returns 403 forbidden error when attempting to downgrade', async () => {
const { body } = await supertest
.get('/api/upgrade_assistant/status')
.query({
targetVersion: '8.0.0',
})
.set('kbn-xsrf', 'xxx')
.expect(403);
expect(body.message).to.be('Forbidden');
});
});
});
}