Skip to content

Commit

Permalink
[On-Week] Hot update of APM/EBT labels (#157093)
Browse files Browse the repository at this point in the history
  • Loading branch information
afharo authored Aug 31, 2023
1 parent 81aceaa commit 0ea37c1
Show file tree
Hide file tree
Showing 37 changed files with 683 additions and 37 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/.github/codeql @elastic/kibana-security
/.github/workflows/codeql.yml @elastic/kibana-security
/src/dev/eslint/security_eslint_rule_tests.ts @elastic/kibana-security
/src/core/server/integration_tests/config/check_dynamic_config.test.ts @elastic/kibana-security
/src/plugins/telemetry/server/config/telemetry_labels.ts @elastic/kibana-security
/test/interactive_setup_api_integration/ @elastic/kibana-security
/test/interactive_setup_functional/ @elastic/kibana-security
Expand Down
3 changes: 3 additions & 0 deletions config/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ elasticsearch.requestHeadersWhitelist: ["authorization", "es-client-authenticati
# Limit maxSockets to 800 as we do in ESS, which improves reliability under high loads.
elasticsearch.maxSockets: 800

# Enable dynamic config to be updated via the internal HTTP requests
coreApp.allowDynamicConfigOverrides: true

# Visualizations editors readonly settings
vis_type_gauge.readOnly: true
vis_type_heatmap.readOnly: true
Expand Down
3 changes: 2 additions & 1 deletion packages/core/apps/core-apps-server-internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* Side Public License, v 1.
*/

export { CoreAppsService } from './src';
export { CoreAppsService, config } from './src';
export type {
CoreAppConfigType,
InternalCoreAppsServiceRequestHandlerContext,
InternalCoreAppsServiceRouter,
} from './src';
Expand Down
50 changes: 42 additions & 8 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 @@ -17,6 +17,7 @@ import { PluginType } from '@kbn/core-base-common';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { coreInternalLifecycleMock } from '@kbn/core-lifecycle-server-mocks';
import { CoreAppsService } from './core_app';
import { of } from 'rxjs';

const emptyPlugins = (): UiPlugins => ({
internal: new Map(),
Expand Down Expand Up @@ -56,10 +57,43 @@ describe('CoreApp', () => {
registerBundleRoutesMock.mockReset();
});

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',
})
);
});

it('is registered 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());

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

describe('`/status` route', () => {
it('is registered with `authRequired: false` is the status page is anonymous', () => {
it('is registered with `authRequired: false` is the status page is anonymous', async () => {
internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(true);
coreApp.setup(internalCoreSetup, emptyPlugins());
await coreApp.setup(internalCoreSetup, emptyPlugins());

expect(httpResourcesRegistrar.register).toHaveBeenCalledWith(
{
Expand All @@ -73,9 +107,9 @@ describe('CoreApp', () => {
);
});

it('is registered with `authRequired: true` is the status page is not anonymous', () => {
it('is registered with `authRequired: true` is the status page is not anonymous', async () => {
internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(false);
coreApp.setup(internalCoreSetup, emptyPlugins());
await coreApp.setup(internalCoreSetup, emptyPlugins());

expect(httpResourcesRegistrar.register).toHaveBeenCalledWith(
{
Expand Down Expand Up @@ -185,8 +219,8 @@ describe('CoreApp', () => {
});

describe('`/app/{id}/{any*}` route', () => {
it('is registered with the correct parameters', () => {
coreApp.setup(internalCoreSetup, emptyPlugins());
it('is registered with the correct parameters', async () => {
await coreApp.setup(internalCoreSetup, emptyPlugins());

expect(httpResourcesRegistrar.register).toHaveBeenCalledWith(
{
Expand All @@ -201,9 +235,9 @@ describe('CoreApp', () => {
});
});

it('`setup` calls `registerBundleRoutes` with the correct options', () => {
it('`setup` calls `registerBundleRoutes` with the correct options', async () => {
const uiPlugins = emptyPlugins();
coreApp.setup(internalCoreSetup, uiPlugins);
await coreApp.setup(internalCoreSetup, uiPlugins);

expect(registerBundleRoutesMock).toHaveBeenCalledTimes(1);
expect(registerBundleRoutesMock).toHaveBeenCalledWith({
Expand Down
68 changes: 63 additions & 5 deletions packages/core/apps/core-apps-server-internal/src/core_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
*/

import { stringify } from 'querystring';
import { Env } from '@kbn/config';
import { schema } from '@kbn/config-schema';
import { Env, IConfigService } from '@kbn/config';
import { schema, ValidationError } from '@kbn/config-schema';
import { fromRoot } from '@kbn/repo-info';
import type { Logger } from '@kbn/logging';
import type { CoreContext } from '@kbn/core-base-server-internal';
Expand All @@ -22,6 +22,8 @@ import type {
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 { firstValueFrom, map, type Observable } from 'rxjs';
import { CoreAppConfig, type CoreAppConfigType, CoreAppPath } from './core_app_config';
import { registerBundleRoutes } from './bundle_routes';
import type { InternalCoreAppsServiceRequestHandlerContext } from './internal_types';

Expand All @@ -41,10 +43,16 @@ interface CommonRoutesParams {
export class CoreAppsService {
private readonly logger: Logger;
private readonly env: Env;
private readonly configService: IConfigService;
private readonly config$: Observable<CoreAppConfig>;

constructor(core: CoreContext) {
this.logger = core.logger.get('core-app');
this.env = core.env;
this.configService = core.configService;
this.config$ = this.configService
.atPath<CoreAppConfigType>(CoreAppPath)
.pipe(map((rawCfg) => new CoreAppConfig(rawCfg)));
}

preboot(corePreboot: InternalCorePreboot, uiPlugins: UiPlugins) {
Expand All @@ -57,9 +65,10 @@ export class CoreAppsService {
}
}

setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) {
async setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) {
this.logger.debug('Setting up core app.');
this.registerDefaultRoutes(coreSetup, uiPlugins);
const config = await firstValueFrom(this.config$);
this.registerDefaultRoutes(coreSetup, uiPlugins, config);
this.registerStaticDirs(coreSetup);
}

Expand Down Expand Up @@ -88,7 +97,11 @@ export class CoreAppsService {
});
}

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

if (config.allowDynamicConfigOverrides) {
this.registerInternalCoreSettingsRoute(router);
}
}

/**
* Registers the HTTP API that allows updating in-memory the settings that opted-in to be dynamically updatable.
* @param router {@link IRouter}
* @private
*/
private registerInternalCoreSettingsRoute(router: IRouter) {
router.versioned
.put({
path: '/internal/core/_settings',
access: 'internal',
options: {
tags: ['access:updateDynamicConfig'],
},
})
.addVersion(
{
version: '1',
validate: {
request: {
body: schema.recordOf(schema.string(), schema.any()),
},
response: {
'200': { body: schema.object({ ok: schema.boolean() }) },
},
},
},
async (context, req, res) => {
try {
this.configService.setDynamicConfigOverrides(req.body);
} catch (err) {
if (err instanceof ValidationError) {
return res.badRequest({ body: err });
}
throw err;
}

return res.ok({ body: { ok: true } });
}
);
}

private registerCommonDefaultRoutes({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.
*/

import { config, CoreAppConfig } from './core_app_config';

describe('CoreApp Config', () => {
test('set correct defaults', () => {
const configValue = new CoreAppConfig(config.schema.validate({}));
expect(configValue).toMatchInlineSnapshot(`
CoreAppConfig {
"allowDynamicConfigOverrides": false,
}
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.
*/

import { schema, type TypeOf } from '@kbn/config-schema';
import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';

/**
* Validation schema for Core App config.
* @public
*/
export const configSchema = schema.object({
allowDynamicConfigOverrides: schema.boolean({ defaultValue: false }),
});

export type CoreAppConfigType = TypeOf<typeof configSchema>;

export const CoreAppPath = 'coreApp';

export const config: ServiceConfigDescriptor<CoreAppConfigType> = {
path: CoreAppPath,
schema: configSchema,
};

/**
* Wrapper of config schema.
* @internal
*/
export class CoreAppConfig implements CoreAppConfigType {
/**
* @internal
* When true, the HTTP API to dynamically extend the configuration is registered.
*
* @remarks
* You should enable this at your own risk: Settings opted-in to being dynamically
* configurable can be changed at any given point, potentially leading to unexpected behaviours.
* This feature is mostly intended for testing purposes.
*/
public readonly allowDynamicConfigOverrides: boolean;

constructor(rawConfig: CoreAppConfig) {
this.allowDynamicConfigOverrides = rawConfig.allowDynamicConfigOverrides;
}
}
1 change: 1 addition & 0 deletions packages/core/apps/core-apps-server-internal/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export { CoreAppsService } from './core_app';
export { config, type CoreAppConfigType } from './core_app_config';
export type {
InternalCoreAppsServiceRequestHandlerContext,
InternalCoreAppsServiceRouter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,14 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
getFlattenedObject(configDescriptor.exposeToUsage)
);
}
if (configDescriptor.dynamicConfig) {
const configKeys = Object.entries(getFlattenedObject(configDescriptor.dynamicConfig))
.filter(([, value]) => value === true)
.map(([key]) => key);
if (configKeys.length > 0) {
this.coreContext.configService.addDynamicConfigPaths(plugin.configPath, configKeys);
}
}
this.coreContext.configService.setSchema(plugin.configPath, configDescriptor.schema);
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/plugins/core-plugins-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
SharedGlobalConfig,
MakeUsageFromSchema,
ExposedToBrowserDescriptor,
DynamicConfigDescriptor,
} from './src';

export { SharedGlobalConfigKeys } from './src';
1 change: 1 addition & 0 deletions packages/core/plugins/core-plugins-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
SharedGlobalConfig,
MakeUsageFromSchema,
ExposedToBrowserDescriptor,
DynamicConfigDescriptor,
} from './types';

export { SharedGlobalConfigKeys } from './shared_global_config';
23 changes: 22 additions & 1 deletion packages/core/plugins/core-plugins-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type PluginConfigSchema<T> = Type<T>;

/**
* Type defining the list of configuration properties that will be exposed on the client-side
* Object properties can either be fully exposed
* Object properties can either be fully exposed or narrowed down to specific keys.
*
* @public
*/
Expand All @@ -49,6 +49,23 @@ export type ExposedToBrowserDescriptor<T> = {
boolean;
};

/**
* Type defining the list of configuration properties that can be dynamically updated
* Object properties can either be fully exposed or narrowed down to specific keys.
*
* @public
*/
export type DynamicConfigDescriptor<T> = {
[Key in keyof T]?: T[Key] extends Maybe<any[]>
? // handles arrays as primitive values
boolean
: T[Key] extends Maybe<object>
? // can be nested for objects
DynamicConfigDescriptor<T[Key]> | boolean
: // primitives
boolean;
};

/**
* Describes a plugin configuration properties.
*
Expand Down Expand Up @@ -88,6 +105,10 @@ export interface PluginConfigDescriptor<T = any> {
* List of configuration properties that will be available on the client-side plugin.
*/
exposeToBrowser?: ExposedToBrowserDescriptor<T>;
/**
* List of configuration properties that can be dynamically changed via the PUT /_settings API.
*/
dynamicConfig?: DynamicConfigDescriptor<T>;
/**
* Schema to use to validate the plugin configuration.
*
Expand Down
Loading

0 comments on commit 0ea37c1

Please sign in to comment.