Skip to content

Commit

Permalink
Merge branch 'main' into enable-process-descendant-filtering-for-main
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine authored Aug 8, 2024
2 parents 779a995 + 1b85455 commit bbf5854
Show file tree
Hide file tree
Showing 34 changed files with 595 additions and 81 deletions.
80 changes: 57 additions & 23 deletions packages/core/apps/core-apps-server-internal/src/core_app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('CoreApp', () => {
let httpResourcesRegistrar: ReturnType<typeof httpResourcesMock.createRegistrar>;

beforeEach(() => {
jest.useFakeTimers();
coreContext = mockCoreContext.create();

internalCorePreboot = coreInternalLifecycleMock.createInternalPreboot();
Expand All @@ -55,37 +56,70 @@ describe('CoreApp', () => {

afterEach(() => {
registerBundleRoutesMock.mockReset();
coreApp.stop();
jest.clearAllTimers();
});

describe('`/internal/core/_settings` route', () => {
it('is not registered by default', async () => {
const routerMock = mockRouter.create();
internalCoreSetup.http.createRouter.mockReturnValue(routerMock);
describe('Dynamic Config feature', () => {
describe('`/internal/core/_settings` route', () => {
it('is not registered by default', async () => {
const routerMock = mockRouter.create();
internalCoreSetup.http.createRouter.mockReturnValue(routerMock);

const localCoreApp = new CoreAppsService(coreContext);
await localCoreApp.setup(internalCoreSetup, emptyPlugins());

expect(routerMock.versioned.put).not.toHaveBeenCalledWith(
expect.objectContaining({
path: '/internal/core/_settings',
})
);

// But the Saved Object is still registered
expect(internalCoreSetup.savedObjects.registerType).toHaveBeenCalledWith(
expect.objectContaining({ name: 'dynamic-config-overrides' })
);
});

it('is registered when enabled', async () => {
const routerMock = mockRouter.create();
internalCoreSetup.http.createRouter.mockReturnValue(routerMock);

const localCoreApp = new CoreAppsService(coreContext);
await localCoreApp.setup(internalCoreSetup, emptyPlugins());
coreContext.configService.atPath.mockReturnValue(of({ allowDynamicConfigOverrides: true }));
const localCoreApp = new CoreAppsService(coreContext);
await localCoreApp.setup(internalCoreSetup, emptyPlugins());

expect(routerMock.versioned.put).not.toHaveBeenCalledWith(
expect.objectContaining({
expect(routerMock.versioned.put).toHaveBeenCalledWith({
path: '/internal/core/_settings',
})
);
});
access: 'internal',
options: {
tags: ['access:updateDynamicConfig'],
},
});
});

it('is registered when enabled', async () => {
const routerMock = mockRouter.create();
internalCoreSetup.http.createRouter.mockReturnValue(routerMock);
it('it fetches the persisted document when enabled', async () => {
const routerMock = mockRouter.create();
internalCoreSetup.http.createRouter.mockReturnValue(routerMock);

coreContext.configService.atPath.mockReturnValue(of({ allowDynamicConfigOverrides: true }));
const localCoreApp = new CoreAppsService(coreContext);
await localCoreApp.setup(internalCoreSetup, emptyPlugins());
coreContext.configService.atPath.mockReturnValue(of({ allowDynamicConfigOverrides: true }));
const localCoreApp = new CoreAppsService(coreContext);
await localCoreApp.setup(internalCoreSetup, emptyPlugins());

expect(routerMock.versioned.put).toHaveBeenCalledWith({
path: '/internal/core/_settings',
access: 'internal',
options: {
tags: ['access:updateDynamicConfig'],
},
const internalCoreStart = coreInternalLifecycleMock.createInternalStart();
localCoreApp.start(internalCoreStart);

expect(internalCoreStart.savedObjects.createInternalRepository).toHaveBeenCalledWith([
'dynamic-config-overrides',
]);

const repository =
internalCoreStart.savedObjects.createInternalRepository.mock.results[0].value;
await jest.advanceTimersByTimeAsync(0); // "Advancing" 0ms is enough, but necessary to trigger the `timer` observable
expect(repository.get).toHaveBeenCalledWith(
'dynamic-config-overrides',
'dynamic-config-overrides'
);
});
});
});
Expand Down
121 changes: 110 additions & 11 deletions packages/core/apps/core-apps-server-internal/src/core_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,27 @@ import type {
} from '@kbn/core-http-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
import type { HttpResources, HttpResourcesServiceToolkit } from '@kbn/core-http-resources-server';
import type { InternalCorePreboot, InternalCoreSetup } from '@kbn/core-lifecycle-server-internal';
import type {
InternalCorePreboot,
InternalCoreSetup,
InternalCoreStart,
} from '@kbn/core-lifecycle-server-internal';
import type { InternalStaticAssets } from '@kbn/core-http-server-internal';
import { firstValueFrom, map, type Observable } from 'rxjs';
import {
combineLatest,
concatMap,
firstValueFrom,
map,
type Observable,
ReplaySubject,
shareReplay,
Subject,
takeUntil,
timer,
} from 'rxjs';
import type { InternalSavedObjectsServiceStart } from '@kbn/core-saved-objects-server-internal';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { CoreAppConfig, type CoreAppConfigType, CoreAppPath } from './core_app_config';
import { registerBundleRoutes } from './bundle_routes';
import type { InternalCoreAppsServiceRequestHandlerContext } from './internal_types';
Expand All @@ -41,12 +59,17 @@ interface CommonRoutesParams {
) => Promise<IKibanaResponse>;
}

const DYNAMIC_CONFIG_OVERRIDES_SO_TYPE = 'dynamic-config-overrides';
const DYNAMIC_CONFIG_OVERRIDES_SO_ID = 'dynamic-config-overrides';

/** @internal */
export class CoreAppsService {
private readonly logger: Logger;
private readonly env: Env;
private readonly configService: IConfigService;
private readonly config$: Observable<CoreAppConfig>;
private readonly savedObjectsStart$ = new ReplaySubject<InternalSavedObjectsServiceStart>(1);
private readonly stop$ = new Subject<void>();

constructor(core: CoreContext) {
this.logger = core.logger.get('core-app');
Expand All @@ -70,8 +93,21 @@ export class CoreAppsService {
async setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) {
this.logger.debug('Setting up core app.');
const config = await firstValueFrom(this.config$);
this.registerDefaultRoutes(coreSetup, uiPlugins, config);
this.registerDefaultRoutes(coreSetup, uiPlugins);
this.registerStaticDirs(coreSetup, uiPlugins);
this.maybeRegisterDynamicConfigurationFeature({
config,
coreSetup,
savedObjectsStart$: this.savedObjectsStart$,
});
}

start(coreStart: InternalCoreStart) {
this.savedObjectsStart$.next(coreStart.savedObjects);
}

stop() {
this.stop$.next();
}

private registerPrebootDefaultRoutes(corePreboot: InternalCorePreboot, uiPlugins: UiPlugins) {
Expand Down Expand Up @@ -100,11 +136,7 @@ export class CoreAppsService {
});
}

private registerDefaultRoutes(
coreSetup: InternalCoreSetup,
uiPlugins: UiPlugins,
config: CoreAppConfig
) {
private registerDefaultRoutes(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) {
const httpSetup = coreSetup.http;
const router = httpSetup.createRouter<InternalCoreAppsServiceRequestHandlerContext>('');
const resources = coreSetup.httpResources.createRegistrar(router);
Expand Down Expand Up @@ -167,18 +199,80 @@ export class CoreAppsService {
}
}
);
}

private maybeRegisterDynamicConfigurationFeature({
config,
coreSetup,
savedObjectsStart$,
}: {
config: CoreAppConfig;
coreSetup: InternalCoreSetup;
savedObjectsStart$: Observable<InternalSavedObjectsServiceStart>;
}) {
// Always registering the Saved Objects to avoid ON/OFF conflicts in the migrations
coreSetup.savedObjects.registerType({
name: DYNAMIC_CONFIG_OVERRIDES_SO_TYPE,
hidden: true,
hiddenFromHttpApis: true,
namespaceType: 'agnostic',
mappings: {
dynamic: false,
properties: {},
},
});

if (config.allowDynamicConfigOverrides) {
this.registerInternalCoreSettingsRoute(router);
const savedObjectsClient$ = savedObjectsStart$.pipe(
map((savedObjectsStart) =>
savedObjectsStart.createInternalRepository([DYNAMIC_CONFIG_OVERRIDES_SO_TYPE])
),
shareReplay(1)
);

// Register the HTTP route
const router = coreSetup.http.createRouter<InternalCoreAppsServiceRequestHandlerContext>('');
this.registerInternalCoreSettingsRoute(router, savedObjectsClient$);

let latestOverrideVersion: string | undefined; // Use the document version to avoid calling override on every poll
// Poll for updates
combineLatest([savedObjectsClient$, timer(0, 10_000)])
.pipe(
concatMap(async ([soClient]) => {
try {
const persistedOverrides = await soClient.get<Record<string, unknown>>(
DYNAMIC_CONFIG_OVERRIDES_SO_TYPE,
DYNAMIC_CONFIG_OVERRIDES_SO_ID
);
if (latestOverrideVersion !== persistedOverrides.version) {
this.configService.setDynamicConfigOverrides(persistedOverrides.attributes);
latestOverrideVersion = persistedOverrides.version;
}
} catch (err) {
// Potential failures:
// - The SO document does not exist (404 error) => no need to log
// - The configuration overrides are invalid => they won't be applied and the validation error will be logged.
if (!SavedObjectsErrorHelpers.isNotFoundError(err)) {
this.logger.warn(`Failed to apply the persisted dynamic config overrides: ${err}`);
}
}
}),
takeUntil(this.stop$)
)
.subscribe();
}
}

/**
* Registers the HTTP API that allows updating in-memory the settings that opted-in to be dynamically updatable.
* @param router {@link IRouter}
* @param savedObjectClient$ An observable of a {@link SavedObjectsClientContract | savedObjects client} that will be used to update the document
* @private
*/
private registerInternalCoreSettingsRoute(router: IRouter) {
private registerInternalCoreSettingsRoute(
router: IRouter,
savedObjectClient$: Observable<SavedObjectsClientContract>
) {
router.versioned
.put({
path: '/internal/core/_settings',
Expand All @@ -201,7 +295,12 @@ export class CoreAppsService {
},
async (context, req, res) => {
try {
this.configService.setDynamicConfigOverrides(req.body);
const newGlobalOverrides = this.configService.setDynamicConfigOverrides(req.body);
const soClient = await firstValueFrom(savedObjectClient$);
await soClient.create(DYNAMIC_CONFIG_OVERRIDES_SO_TYPE, newGlobalOverrides, {
id: DYNAMIC_CONFIG_OVERRIDES_SO_ID,
overwrite: true,
});
} catch (err) {
if (err instanceof ValidationError) {
return res.badRequest({ body: err });
Expand Down
3 changes: 3 additions & 0 deletions packages/core/apps/core-apps-server-internal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"@kbn/monaco",
"@kbn/core-http-server-internal",
"@kbn/core-http-router-server-internal",
"@kbn/core-saved-objects-server-internal",
"@kbn/core-saved-objects-api-server",
"@kbn/core-saved-objects-server",
],
"exclude": [
"target/**/*",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/root/core-root-server-internal/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ export class Server {
userProfile: userProfileStart,
};

this.coreApp.start(this.coreStart);

await this.plugins.start(this.coreStart);

await this.http.start();
Expand All @@ -469,6 +471,7 @@ export class Server {
public async stop() {
this.log.debug('stopping server');

this.coreApp.stop();
await this.analytics.stop();
await this.http.stop(); // HTTP server has to stop before savedObjects and ES clients are closed to be able to gracefully attempt to resolve any pending requests
await this.plugins.stop();
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-check-mappings-update-cli/current_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
"title",
"version"
],
"dynamic-config-overrides": [],
"endpoint:unified-user-artifact-manifest": [
"artifactIds",
"policyId",
Expand Down
4 changes: 4 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@
}
}
},
"dynamic-config-overrides": {
"dynamic": false,
"properties": {}
},
"endpoint:unified-user-artifact-manifest": {
"dynamic": false,
"properties": {
Expand Down
15 changes: 15 additions & 0 deletions packages/kbn-config/src/config_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,4 +726,19 @@ describe('Dynamic Overrides', () => {
await firstValueFrom(configService.getConfig$().pipe(map((cfg) => cfg.toRaw())))
).toStrictEqual({ namespace1: { key: 'another-value' } });
});

test('is able to remove a field when setting it to `null`', async () => {
configService.addDynamicConfigPaths('namespace1', ['key']);
configService.setDynamicConfigOverrides({ 'namespace1.key': 'another-value' });

expect(
await firstValueFrom(configService.getConfig$().pipe(map((cfg) => cfg.toRaw())))
).toStrictEqual({ namespace1: { key: 'another-value' } });

configService.setDynamicConfigOverrides({ 'namespace1.key': null });

expect(
await firstValueFrom(configService.getConfig$().pipe(map((cfg) => cfg.toRaw())))
).toStrictEqual({ namespace1: {} });
});
});
Loading

0 comments on commit bbf5854

Please sign in to comment.