Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[On-Week] Hot update of APM/EBT labels #157093

Merged
merged 50 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
62c9016
[On-Week] Hot update of APM/EBT labels
afharo May 8, 2023
e58aa4e
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine May 8, 2023
7644cfa
Use custom apm-agent-nodejs https://github.com/elastic/apm-agent-node…
afharo May 10, 2023
17b34e9
Merge branch 'onweek/hot-update-of-apm-labels' of github.com:afharo/k…
afharo May 10, 2023
4b94138
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine May 10, 2023
da5a9b0
Add new method to APM mocks
afharo May 10, 2023
be56393
Merge branch 'onweek/hot-update-of-apm-labels' of github.com:afharo/k…
afharo May 10, 2023
e031ed0
Spread the update to the browser
afharo May 10, 2023
1c1062b
Spread initial config to the APM global labels
afharo May 10, 2023
1cc38fb
Spread the labels updates to EBT context
afharo May 10, 2023
4ba34c1
update journey tearUp hook
dmlemeshko May 11, 2023
02b0967
Allow unauthenticated requests to telemetry/config API
afharo May 11, 2023
b7370e0
Merge branch 'onweek/hot-update-of-apm-labels' of github.com:afharo/k…
afharo May 11, 2023
dea544b
add wait and move api call
dmlemeshko May 15, 2023
7f7fc21
Merge remote-tracking branch 'upstream/main' into onweek/hot-update-o…
dmlemeshko May 15, 2023
d1adabc
Introduce the concept of dynamic configuration in the core Config Ser…
afharo May 15, 2023
41b9065
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine May 15, 2023
464aa4f
Add/fix tests
afharo May 16, 2023
d45ea1b
Small improvements after self-review
afharo May 16, 2023
7f05ea9
Fix ConfigService mock
afharo May 16, 2023
8069850
Update exposed configs to anonymous pages
afharo May 16, 2023
f956a6a
Export missing type from the index
afharo May 16, 2023
d328290
Merge branch 'main' into onweek/hot-update-of-apm-labels
kibanamachine May 16, 2023
0b90a33
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo May 22, 2023
fb54e70
Address initial feedback
afharo May 22, 2023
670f9f5
Do not swallow errors
afharo May 22, 2023
7415208
Use `main` from apm-nodejs-agent until released
afharo May 23, 2023
be0084d
Update access tag
afharo May 23, 2023
7d2fff6
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo May 23, 2023
8c1f684
Point APM to the gh repo until it is published
afharo May 23, 2023
2d04781
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine May 23, 2023
7b80030
clean kbn-journey & dataset-extractor
dmlemeshko May 25, 2023
1543062
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo Jun 6, 2023
ecc1381
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo Aug 4, 2023
bdadfab
Hide behind feature-flag + add tests
afharo Aug 4, 2023
d668f76
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo Aug 7, 2023
1d58c84
Fix tests
afharo Aug 7, 2023
666a37c
Journey FTR harness to set the appropriate internal headers
afharo Aug 7, 2023
f768e8c
Remove accidental duplicated CLI option
afharo Aug 7, 2023
97da881
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 7, 2023
51f51c9
Merge branch 'main' into onweek/hot-update-of-apm-labels
afharo Aug 7, 2023
f3fbe27
Merge branch 'main' of github.com:elastic/kibana into onweek/hot-upda…
afharo Aug 23, 2023
2bbc917
Add test to ping us when new options opt-in
afharo Aug 25, 2023
c6314f8
Merge branch 'main' into onweek/hot-update-of-apm-labels
afharo Aug 25, 2023
19f114c
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 25, 2023
730ebbc
Explicit `string` type to help TS inferrence
afharo Aug 25, 2023
0fcc9e0
Add missing field to the telemetry/config response schema
afharo Aug 28, 2023
d5d42a8
Merge branch 'main' into onweek/hot-update-of-apm-labels
afharo Aug 28, 2023
4e31733
Merge branch 'main' into onweek/hot-update-of-apm-labels
dmlemeshko Aug 29, 2023
acb62bb
Merge branch 'main' into onweek/hot-update-of-apm-labels
dmlemeshko Aug 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,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 @@ -86,6 +86,9 @@ console.autocompleteDefinitions.endpointsAvailability: serverless
# Allow authentication via the Elasticsearch JWT realm with the `shared_secret` client authentication type.
elasticsearch.requestHeadersWhitelist: ["authorization", "es-client-authentication"]

# 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 {
Copy link
Contributor

@pgayvallet pgayvallet May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(unrelated to this file, just writing as a comment to allow replying on the thread)

Overall, the technical implementation makes sense, and I don't have any strong problem regarding it.

My concerns / questions are more regarding the usages we're planning on doing of this feature.

We're basically implementing a per-node hot configuration update here. Which can be great. Now, these hot changes are not persisted. Any node rebooting will loose those hot updates, and reboot with the 'initial' configuration.

Which makes me wonder if we're planning any concrete production usage of the feature, or if the only planned usage is to update those labels in testing environments.

Because if that's the latest, then I can't avoid wondering if having that as a core API / endpoint makes sense compared to have it as a plugin that would just be in charge of updating the APM and EBT labels accordingly, without having to plug the feature so low level in core's config service.

OTOH, being able somehow to update dynamically some parts the config more easily than how it's currently done (I'm thinking, idk, about enabling debug logging via this endpoint, for instance) could be extremely powerful.

Curious to know what you think about this?

(also, if we're planning on merging, I think we're missing unit/functional/ftr tests on parts of the code that was modified in the PR)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR, Yes to all! 😇

I can't avoid wondering if having that as a core API / endpoint makes sense compared to have it as a plugin that would just be in charge of updating the APM and EBT labels accordingly

I initially implemented that. The main reason I ended up moving it to core was because the plugin couldn't inject the values to the initially rendered page, so APM and EBT initial events like page-load were wrongly labeled. Refer to Alternative options considered in the description.

OTOH, being able somehow to update dynamically some parts the config more easily than how it's currently done (I'm thinking, idk, about enabling debug logging via this endpoint, for instance) could be extremely powerful.

++ that'd be a great use case for this feature.

Curious to know what you think about this?

TBH, I have mixed feelings about this solution, especially for the caveats highlighted in What's not that good about this solution?. On the flip side, I think it's the best solution we can provide, given the current architecture.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for your explanation.

In that case, it could be a good idea to talk about it with the team in one of meeting to see what the others think about it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, it could be a good idea to talk about it with the team in one of meeting to see what the others think about it?

Yeah! In our last sync, I asked the team to have a look at this PR and the description to figure out if this is something we want to support, or if they can help me come up with alternatives.

I'll bring it up again in our next sync :)

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 });
}
afharo marked this conversation as resolved.
Show resolved Hide resolved
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