[uiSettings] always use the latest config document to create the new one (#159649)

## Summary

Fix https://github.com/elastic/kibana/issues/159646

Fix the config creation-from-previous-one logic by always using the
latest config for the new version's creation


## Release Note

Fix a bug that could cause old Kibana deployments to loose their
uiSettings after an upgrade

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pierre Gayvallet 2023-06-19 03:09:27 -04:00 committed by GitHub
parent 9ea864cc05
commit 83abc6e3c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 100 additions and 17 deletions

View file

@ -9,6 +9,7 @@
import { Subject, Observable, firstValueFrom, of } from 'rxjs'; import { Subject, Observable, firstValueFrom, of } from 'rxjs';
import { filter, take, switchMap } from 'rxjs/operators'; import { filter, take, switchMap } from 'rxjs/operators';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import { stripVersionQualifier } from '@kbn/std';
import type { ServiceStatus } from '@kbn/core-status-common'; import type { ServiceStatus } from '@kbn/core-status-common';
import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
import type { DocLinksServiceStart } from '@kbn/core-doc-links-server'; import type { DocLinksServiceStart } from '@kbn/core-doc-links-server';
@ -106,9 +107,7 @@ export class SavedObjectsService
constructor(private readonly coreContext: CoreContext) { constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get('savedobjects-service'); this.logger = coreContext.logger.get('savedobjects-service');
this.kibanaVersion = SavedObjectsService.stripVersionQualifier( this.kibanaVersion = stripVersionQualifier(this.coreContext.env.packageInfo.version);
this.coreContext.env.packageInfo.version
);
} }
public async setup(setupDeps: SavedObjectsSetupDeps): Promise<InternalSavedObjectsServiceSetup> { public async setup(setupDeps: SavedObjectsSetupDeps): Promise<InternalSavedObjectsServiceSetup> {
@ -384,12 +383,4 @@ export class SavedObjectsService
nodeRoles: nodeInfo.roles, 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];
}
} }

View file

@ -46,6 +46,7 @@
"@kbn/utils", "@kbn/utils",
"@kbn/core-http-router-server-internal", "@kbn/core-http-router-server-internal",
"@kbn/logging-mocks", "@kbn/logging-mocks",
"@kbn/std",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -46,6 +46,59 @@ describe('getUpgradeableConfig', () => {
expect(result).toEqual(savedConfig); 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 () => { it('finds saved config with RC version === Kibana version', async () => {
const savedConfig = { id: '7.5.0-rc1', attributes: 'foo' }; const savedConfig = { id: '7.5.0-rc1', attributes: 'foo' };
const savedObjectsClient = savedObjectsClientMock.create(); const savedObjectsClient = savedObjectsClientMock.create();

View file

@ -6,7 +6,11 @@
* Side Public License, v 1. * 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 type { ConfigAttributes } from '../saved_objects';
import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable';
@ -47,11 +51,26 @@ export async function getUpgradeableConfig({
}); });
// try to find a config that we can upgrade // try to find a config that we can upgrade
const findResult = savedConfigs.find((savedConfig) => const matchingResults = savedConfigs.filter((savedConfig) =>
isConfigVersionUpgradeable(savedConfig.id, version) isConfigVersionUpgradeable(savedConfig.id, version)
); );
if (findResult) { const mostRecentConfig = getMostRecentConfig(matchingResults);
return { id: findResult.id, attributes: findResult.attributes }; if (mostRecentConfig) {
return { id: mostRecentConfig.id, attributes: mostRecentConfig.attributes };
} }
return null; return null;
} }
const getMostRecentConfig = (
results: Array<SavedObjectsFindResult<UpgradeableConfigAttributes>>
): SavedObjectsFindResult<UpgradeableConfigAttributes> | undefined => {
return results.reduce<SavedObjectsFindResult<UpgradeableConfigAttributes> | undefined>(
(mostRecent, current) => {
if (!mostRecent) {
return current;
}
return Semver.gt(mostRecent.id, current.id) ? mostRecent : current;
},
undefined
);
};

View file

@ -10,6 +10,7 @@ import { firstValueFrom, Observable } from 'rxjs';
import { mapToObject } from '@kbn/std'; import { mapToObject } from '@kbn/std';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import { stripVersionQualifier } from '@kbn/std';
import type { CoreContext, CoreService } from '@kbn/core-base-server-internal'; import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal'; import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
@ -32,6 +33,7 @@ export interface SetupDeps {
http: InternalHttpServiceSetup; http: InternalHttpServiceSetup;
savedObjects: InternalSavedObjectsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup;
} }
type ClientType<T> = T extends 'global' type ClientType<T> = T extends 'global'
? UiSettingsGlobalClient ? UiSettingsGlobalClient
: T extends 'namespace' : T extends 'namespace'
@ -109,7 +111,7 @@ export class UiSettingsService
const isNamespaceScope = scope === 'namespace'; const isNamespaceScope = scope === 'namespace';
const options = { const options = {
type: (isNamespaceScope ? 'config' : 'config-global') as 'config' | 'config-global', type: (isNamespaceScope ? 'config' : 'config-global') as 'config' | 'config-global',
id: version, id: stripVersionQualifier(version),
buildNum, buildNum,
savedObjectsClient, savedObjectsClient,
defaults: isNamespaceScope defaults: isNamespaceScope

View file

@ -29,3 +29,4 @@ export {
} from './src/iteration'; } from './src/iteration';
export { ensureDeepObject } from './src/ensure_deep_object'; export { ensureDeepObject } from './src/ensure_deep_object';
export { Semaphore } from './src/semaphore'; export { Semaphore } from './src/semaphore';
export { stripVersionQualifier } from './src/strip_version_qualifier';

View file

@ -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];
}

View file

@ -5,6 +5,7 @@
* 2.0. * 2.0.
*/ */
import { stripVersionQualifier } from '@kbn/std';
import { FtrProviderContext } from '../../ftr_provider_context'; import { FtrProviderContext } from '../../ftr_provider_context';
export default function enterSpaceFunctionalTests({ export default function enterSpaceFunctionalTests({
@ -32,7 +33,7 @@ export default function enterSpaceFunctionalTests({
{ space: 'another-space' } { space: 'another-space' }
); );
const config = await kibanaServer.savedObjects.get({ const config = await kibanaServer.savedObjects.get({
id: await kibanaServer.version.get(), id: stripVersionQualifier(await kibanaServer.version.get()),
type: 'config', type: 'config',
}); });
await kibanaServer.savedObjects.update({ await kibanaServer.savedObjects.update({