diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts index 51f7f29e9683a..860284a892fb1 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/saved_objects_service.ts @@ -9,6 +9,7 @@ import { Subject, Observable, firstValueFrom, of } from 'rxjs'; import { filter, take, switchMap } from 'rxjs/operators'; import type { Logger } from '@kbn/logging'; +import { stripVersionQualifier } from '@kbn/std'; import type { ServiceStatus } from '@kbn/core-status-common'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; import type { DocLinksServiceStart } from '@kbn/core-doc-links-server'; @@ -106,9 +107,7 @@ export class SavedObjectsService constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); - this.kibanaVersion = SavedObjectsService.stripVersionQualifier( - this.coreContext.env.packageInfo.version - ); + this.kibanaVersion = stripVersionQualifier(this.coreContext.env.packageInfo.version); } public async setup(setupDeps: SavedObjectsSetupDeps): Promise { @@ -384,12 +383,4 @@ export class SavedObjectsService nodeRoles: nodeInfo.roles, }); } - - /** - * Coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) - * to regular semver (x.y.z). - */ - private static stripVersionQualifier(version: string) { - return version.split('-')[0]; - } } diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/tsconfig.json b/packages/core/saved-objects/core-saved-objects-server-internal/tsconfig.json index 7f6d935a488b7..bd5c290172794 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/tsconfig.json +++ b/packages/core/saved-objects/core-saved-objects-server-internal/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/utils", "@kbn/core-http-router-server-internal", "@kbn/logging-mocks", + "@kbn/std", ], "exclude": [ "target/**/*", diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.test.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.test.ts index bc6e427cd2a57..ba7fd1b682f27 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.test.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.test.ts @@ -46,6 +46,59 @@ describe('getUpgradeableConfig', () => { expect(result).toEqual(savedConfig); }); + it('uses the latest config when multiple are found', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { id: '7.2.0', attributes: 'foo' }, + { id: '7.3.0', attributes: 'foo' }, + ], + } as SavedObjectsFindResponse); + + const result = await getUpgradeableConfig({ + savedObjectsClient, + version: '7.5.0', + type: 'config', + }); + expect(result!.id).toBe('7.3.0'); + }); + + it('uses the latest config when multiple are found with rc qualifier', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { id: '7.2.0', attributes: 'foo' }, + { id: '7.3.0', attributes: 'foo' }, + { id: '7.5.0-rc1', attributes: 'foo' }, + ], + } as SavedObjectsFindResponse); + + const result = await getUpgradeableConfig({ + savedObjectsClient, + version: '7.5.0', + type: 'config', + }); + expect(result!.id).toBe('7.5.0-rc1'); + }); + + it('ignores documents with malformed ids', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { id: 'not-a-semver', attributes: 'foo' }, + { id: '7.2.0', attributes: 'foo' }, + { id: '7.3.0', attributes: 'foo' }, + ], + } as SavedObjectsFindResponse); + + const result = await getUpgradeableConfig({ + savedObjectsClient, + version: '7.5.0', + type: 'config', + }); + expect(result!.id).toBe('7.3.0'); + }); + it('finds saved config with RC version === Kibana version', async () => { const savedConfig = { id: '7.5.0-rc1', attributes: 'foo' }; const savedObjectsClient = savedObjectsClientMock.create(); diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.ts index 18479db1ebd28..ad7572095796e 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import Semver from 'semver'; +import type { + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '@kbn/core-saved-objects-api-server'; import type { ConfigAttributes } from '../saved_objects'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; @@ -47,11 +51,26 @@ export async function getUpgradeableConfig({ }); // try to find a config that we can upgrade - const findResult = savedConfigs.find((savedConfig) => + const matchingResults = savedConfigs.filter((savedConfig) => isConfigVersionUpgradeable(savedConfig.id, version) ); - if (findResult) { - return { id: findResult.id, attributes: findResult.attributes }; + const mostRecentConfig = getMostRecentConfig(matchingResults); + if (mostRecentConfig) { + return { id: mostRecentConfig.id, attributes: mostRecentConfig.attributes }; } return null; } + +const getMostRecentConfig = ( + results: Array> +): SavedObjectsFindResult | undefined => { + return results.reduce | undefined>( + (mostRecent, current) => { + if (!mostRecent) { + return current; + } + return Semver.gt(mostRecent.id, current.id) ? mostRecent : current; + }, + undefined + ); +}; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts index e4dabb6b50f46..473e7569e2179 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts @@ -10,6 +10,7 @@ import { firstValueFrom, Observable } from 'rxjs'; import { mapToObject } from '@kbn/std'; import type { Logger } from '@kbn/logging'; +import { stripVersionQualifier } from '@kbn/std'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; @@ -32,6 +33,7 @@ export interface SetupDeps { http: InternalHttpServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; } + type ClientType = T extends 'global' ? UiSettingsGlobalClient : T extends 'namespace' @@ -109,7 +111,7 @@ export class UiSettingsService const isNamespaceScope = scope === 'namespace'; const options = { type: (isNamespaceScope ? 'config' : 'config-global') as 'config' | 'config-global', - id: version, + id: stripVersionQualifier(version), buildNum, savedObjectsClient, defaults: isNamespaceScope diff --git a/packages/kbn-std/index.ts b/packages/kbn-std/index.ts index 6a138a96eac38..2a39de500cb25 100644 --- a/packages/kbn-std/index.ts +++ b/packages/kbn-std/index.ts @@ -29,3 +29,4 @@ export { } from './src/iteration'; export { ensureDeepObject } from './src/ensure_deep_object'; export { Semaphore } from './src/semaphore'; +export { stripVersionQualifier } from './src/strip_version_qualifier'; diff --git a/packages/kbn-std/src/strip_version_qualifier.ts b/packages/kbn-std/src/strip_version_qualifier.ts new file mode 100644 index 0000000000000..e83a57a270587 --- /dev/null +++ b/packages/kbn-std/src/strip_version_qualifier.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) + * to regular semver (x.y.z). + */ +export function stripVersionQualifier(version: string): string { + return version.split('-')[0]; +} diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 4e7655add8667..caae7bad10b0f 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { stripVersionQualifier } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function enterSpaceFunctionalTests({ @@ -32,7 +33,7 @@ export default function enterSpaceFunctionalTests({ { space: 'another-space' } ); const config = await kibanaServer.savedObjects.get({ - id: await kibanaServer.version.get(), + id: stripVersionQualifier(await kibanaServer.version.get()), type: 'config', }); await kibanaServer.savedObjects.update({