Add CI check to ensure SO mapping addition are done correctly (#172056)

## Summary

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

Add a CI check verifying that any mapping addition (done after a type's
initial introduction) correctly defines the added mappings as a
`mappings_addition` change in a model version of the owning type (or
throws otherwise)

Similar to https://github.com/elastic/kibana/pull/169610

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pierre Gayvallet 2023-11-30 13:37:11 +01:00 committed by GitHub
parent c96b63c5a2
commit 62d0ce4c7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1892 additions and 82 deletions

View file

@ -42,7 +42,14 @@ export type {
MigrationStatus,
MigrateDocumentOptions,
} from './src/migration';
export { parseObjectKey, getObjectKey, getIndexForType } from './src/utils';
export {
parseObjectKey,
getObjectKey,
getIndexForType,
getFieldListFromTypeMapping,
getFieldListMapFromMappingDefinitions,
type FieldListMap,
} from './src/utils';
export {
modelVersionVirtualMajor,
globalSwitchToModelVersionAt,
@ -68,4 +75,6 @@ export {
buildModelVersionTransformFn,
aggregateMappingAdditions,
convertModelVersionBackwardConversionSchema,
getVersionAddedMappings,
getVersionAddedFields,
} from './src/model_version';

View file

@ -37,3 +37,4 @@ export { getModelVersionDelta } from './get_version_delta';
export { buildModelVersionTransformFn } from './build_transform_fn';
export { aggregateMappingAdditions } from './aggregate_model_changes';
export { convertModelVersionBackwardConversionSchema } from './backward_conversion_schema';
export { getVersionAddedFields, getVersionAddedMappings } from './version_mapping_changes';

View file

@ -0,0 +1,169 @@
/*
* 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 type {
SavedObjectsModelVersion,
SavedObjectsModelChange,
} from '@kbn/core-saved-objects-server';
import { getVersionAddedMappings, getVersionAddedFields } from './version_mapping_changes';
const createVersion = (changes: SavedObjectsModelChange[]): SavedObjectsModelVersion => {
return {
changes,
};
};
describe('getVersionAddedMappings', () => {
it('returns empty mappings when the version has no changes', () => {
const version = createVersion([]);
expect(getVersionAddedMappings(version)).toEqual({});
});
it('returns empty mappings when the version has no `mappings_addition` changes', () => {
const version = createVersion([
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
]);
expect(getVersionAddedMappings(version)).toEqual({});
});
it(`returns the change's mappings when the version has a single 'mappings_addition' changes`, () => {
const version = createVersion([
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
{
type: 'mappings_addition',
addedMappings: {
nested: {
properties: {
foo: { type: 'text' },
},
},
},
},
]);
expect(getVersionAddedMappings(version)).toEqual({
nested: {
properties: {
foo: { type: 'text' },
},
},
});
});
it(`merges the mappings when the version has multiple 'mappings_addition' changes`, () => {
const version = createVersion([
{
type: 'mappings_addition',
addedMappings: {
top: { type: 'text' },
nested: {
properties: {
bar: { type: 'text' },
},
},
},
},
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
{
type: 'mappings_addition',
addedMappings: {
nested: {
properties: {
foo: { type: 'text' },
},
},
},
},
]);
expect(getVersionAddedMappings(version)).toEqual({
top: { type: 'text' },
nested: {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
},
},
});
});
});
describe('getVersionAddedFields', () => {
it('returns empty mappings when the version has no changes', () => {
const version = createVersion([]);
expect(getVersionAddedFields(version)).toEqual([]);
});
it('returns empty mappings when the version has no `mappings_addition` changes', () => {
const version = createVersion([
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
]);
expect(getVersionAddedFields(version)).toEqual([]);
});
it(`returns the change's mappings when the version has a single 'mappings_addition' changes`, () => {
const version = createVersion([
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
{
type: 'mappings_addition',
addedMappings: {
nested: {
properties: {
foo: { type: 'text' },
},
},
},
},
]);
expect(getVersionAddedFields(version)).toEqual(['nested', 'nested.foo']);
});
it(`merges the mappings when the version has multiple 'mappings_addition' changes`, () => {
const version = createVersion([
{
type: 'mappings_addition',
addedMappings: {
top: { type: 'text' },
nested: {
properties: {
bar: { type: 'text' },
},
},
},
},
{
type: 'data_backfill',
backfillFn: jest.fn(),
},
{
type: 'mappings_addition',
addedMappings: {
nested: {
properties: {
foo: { type: 'text' },
},
},
},
},
]);
expect(getVersionAddedFields(version)).toEqual(['nested', 'nested.bar', 'nested.foo', 'top']);
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 { merge } from 'lodash';
import type {
SavedObjectsMappingProperties,
SavedObjectsModelVersion,
SavedObjectsModelMappingsAdditionChange,
} from '@kbn/core-saved-objects-server';
import { getFieldListFromTypeMapping } from '../utils/get_field_list';
/**
* Return the mappings that were introduced in the given version.
* If multiple 'mappings_addition' changes are present for the version,
* they will be deep-merged.
*/
export const getVersionAddedMappings = (
version: SavedObjectsModelVersion
): SavedObjectsMappingProperties => {
const mappingChanges = version.changes.filter(
(change) => change.type === 'mappings_addition'
) as SavedObjectsModelMappingsAdditionChange[];
return merge({}, ...mappingChanges.map((change) => change.addedMappings));
};
/**
* Return the list of fields, sorted, that were introduced in the given version.
*/
export const getVersionAddedFields = (version: SavedObjectsModelVersion): string[] => {
const addedMappings = getVersionAddedMappings(version);
return getFieldListFromTypeMapping({ properties: addedMappings });
};

View file

@ -0,0 +1,69 @@
/*
* 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 { SavedObjectsTypeMappingDefinition } from '@kbn/core-saved-objects-server';
import { getFieldListFromTypeMapping } from './get_field_list';
describe('getFieldListFromTypeMapping', () => {
it('returns an empty list for empty mappings', () => {
const mappings: SavedObjectsTypeMappingDefinition = {
properties: {},
};
expect(getFieldListFromTypeMapping(mappings)).toEqual([]);
});
it('returns the correct list for top level fields', () => {
const mappings: SavedObjectsTypeMappingDefinition = {
properties: {
foo: { type: 'text' },
bar: { type: 'text' },
},
};
expect(getFieldListFromTypeMapping(mappings)).toEqual(['bar', 'foo']);
});
it('returns the correct list for deep fields', () => {
const mappings: SavedObjectsTypeMappingDefinition = {
properties: {
foo: {
properties: {
hello: { type: 'text' },
dolly: { type: 'text' },
},
},
bar: { type: 'text' },
},
};
expect(getFieldListFromTypeMapping(mappings)).toEqual(['bar', 'foo', 'foo.dolly', 'foo.hello']);
});
it('returns the correct list for any depth', () => {
const mappings: SavedObjectsTypeMappingDefinition = {
properties: {
foo: {
properties: {
hello: { type: 'text' },
dolly: {
properties: {
far: { type: 'text' },
},
},
},
},
bar: { type: 'text' },
},
};
expect(getFieldListFromTypeMapping(mappings)).toEqual([
'bar',
'foo',
'foo.dolly',
'foo.dolly.far',
'foo.hello',
]);
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { MappingProperty as EsMappingProperty } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type {
SavedObjectsTypeMappingDefinition,
SavedObjectsFieldMapping,
} from '@kbn/core-saved-objects-server';
import type { SavedObjectsTypeMappingDefinitions } from '../mappings';
export type FieldListMap = Record<string, string[]>;
/**
* Return the list of fields present in each individual type mappings present in the definition.
*/
export const getFieldListMapFromMappingDefinitions = (
mappings: SavedObjectsTypeMappingDefinitions
): FieldListMap => {
return Object.entries(mappings).reduce<FieldListMap>((memo, [typeName, typeMappings]) => {
memo[typeName] = getFieldListFromTypeMapping(typeMappings);
return memo;
}, {});
};
type AnyFieldMapping = SavedObjectsFieldMapping | EsMappingProperty;
interface QueueItem {
fieldPath: string[];
fieldDef: AnyFieldMapping;
}
/**
* Return the list of fields present in the provided mappings.
* Note that fields only containing properties are still considered fields by this function.
*
* @example
* ```
* getFieldListFromTypeMapping({
* properties: {
* foo: {
* properties: {
* hello: { type: 'text' },
* dolly: { type: 'text' },
* },
* },
* },
* });
* // ['foo', 'foo.dolly', 'foo.hello']
* ```
*/
export const getFieldListFromTypeMapping = (
typeMappings: SavedObjectsTypeMappingDefinition
): string[] => {
const fieldList: string[] = [];
const queue: QueueItem[] = [];
Object.entries(typeMappings.properties).forEach(([fieldName, fieldDef]) => {
queue.push({
fieldPath: [fieldName],
fieldDef,
});
});
while (queue.length > 0) {
const item = queue.pop()!;
fieldList.push(item.fieldPath.join('.'));
if ('properties' in item.fieldDef) {
Object.entries(item.fieldDef.properties ?? {}).forEach(([fieldName, fieldDef]) => {
queue.push({
fieldPath: [...item.fieldPath, fieldName],
fieldDef,
});
});
}
}
return fieldList.sort();
};

View file

@ -8,3 +8,8 @@
export { getObjectKey, parseObjectKey } from './object_key';
export { getIndexForType } from './get_index_for_type';
export {
getFieldListFromTypeMapping,
getFieldListMapFromMappingDefinitions,
type FieldListMap,
} from './get_field_list';

View file

@ -0,0 +1,970 @@
{
"core-usage-stats": [],
"legacy-url-alias": [
"disabled",
"resolveCounter",
"sourceId",
"targetId",
"targetNamespace",
"targetType"
],
"config": [
"buildNum"
],
"config-global": [
"buildNum"
],
"url": [
"accessDate",
"createDate",
"slug"
],
"usage-counters": [
"domainId"
],
"task": [
"attempts",
"enabled",
"ownerId",
"retryAt",
"runAt",
"schedule",
"schedule.interval",
"scheduledAt",
"scope",
"status",
"taskType"
],
"guided-onboarding-guide-state": [
"guideId",
"isActive"
],
"guided-onboarding-plugin-state": [],
"ui-metric": [
"count"
],
"application_usage_totals": [],
"application_usage_daily": [
"timestamp"
],
"event_loop_delays_daily": [
"lastUpdatedAt"
],
"index-pattern": [
"name",
"title",
"type"
],
"sample-data-telemetry": [
"installCount",
"unInstallCount"
],
"space": [
"name"
],
"spaces-usage-stats": [],
"exception-list-agnostic": [
"_tags",
"comments",
"comments.comment",
"comments.created_at",
"comments.created_by",
"comments.id",
"comments.updated_at",
"comments.updated_by",
"created_at",
"created_by",
"description",
"entries",
"entries.entries",
"entries.entries.field",
"entries.entries.operator",
"entries.entries.type",
"entries.entries.value",
"entries.field",
"entries.list",
"entries.list.id",
"entries.list.type",
"entries.operator",
"entries.type",
"entries.value",
"expire_time",
"immutable",
"item_id",
"list_id",
"list_type",
"meta",
"name",
"os_types",
"tags",
"tie_breaker_id",
"type",
"updated_by",
"version"
],
"exception-list": [
"_tags",
"comments",
"comments.comment",
"comments.created_at",
"comments.created_by",
"comments.id",
"comments.updated_at",
"comments.updated_by",
"created_at",
"created_by",
"description",
"entries",
"entries.entries",
"entries.entries.field",
"entries.entries.operator",
"entries.entries.type",
"entries.entries.value",
"entries.field",
"entries.list",
"entries.list.id",
"entries.list.type",
"entries.operator",
"entries.type",
"entries.value",
"expire_time",
"immutable",
"item_id",
"list_id",
"list_type",
"meta",
"name",
"os_types",
"tags",
"tie_breaker_id",
"type",
"updated_by",
"version"
],
"telemetry": [],
"file": [
"FileKind",
"Meta",
"Status",
"Updated",
"created",
"extension",
"hash",
"mime_type",
"name",
"size",
"user"
],
"fileShare": [
"created",
"name",
"token",
"valid_until"
],
"action": [
"actionTypeId",
"name"
],
"action_task_params": [],
"connector_token": [
"connectorId",
"tokenType"
],
"query": [
"description",
"title"
],
"kql-telemetry": [],
"search-session": [
"created",
"realmName",
"realmType",
"sessionId",
"username"
],
"search-telemetry": [],
"file-upload-usage-collection-telemetry": [
"file_upload",
"file_upload.index_creation_count"
],
"apm-indices": [],
"tag": [
"color",
"description",
"name"
],
"alert": [
"actions",
"actions.actionRef",
"actions.actionTypeId",
"actions.group",
"alertTypeId",
"consumer",
"createdAt",
"createdBy",
"enabled",
"executionStatus",
"executionStatus.error",
"executionStatus.error.message",
"executionStatus.error.reason",
"executionStatus.lastDuration",
"executionStatus.lastExecutionDate",
"executionStatus.numberOfTriggeredActions",
"executionStatus.status",
"executionStatus.warning",
"executionStatus.warning.message",
"executionStatus.warning.reason",
"lastRun",
"lastRun.alertsCount",
"lastRun.alertsCount.active",
"lastRun.alertsCount.ignored",
"lastRun.alertsCount.new",
"lastRun.alertsCount.recovered",
"lastRun.outcome",
"lastRun.outcomeOrder",
"legacyId",
"mapped_params",
"mapped_params.risk_score",
"mapped_params.severity",
"monitoring",
"monitoring.run",
"monitoring.run.calculated_metrics",
"monitoring.run.calculated_metrics.p50",
"monitoring.run.calculated_metrics.p95",
"monitoring.run.calculated_metrics.p99",
"monitoring.run.calculated_metrics.success_ratio",
"monitoring.run.last_run",
"monitoring.run.last_run.metrics",
"monitoring.run.last_run.metrics.duration",
"monitoring.run.last_run.metrics.gap_duration_s",
"monitoring.run.last_run.metrics.total_alerts_created",
"monitoring.run.last_run.metrics.total_alerts_detected",
"monitoring.run.last_run.metrics.total_indexing_duration_ms",
"monitoring.run.last_run.metrics.total_search_duration_ms",
"monitoring.run.last_run.timestamp",
"muteAll",
"mutedInstanceIds",
"name",
"notifyWhen",
"params",
"revision",
"running",
"schedule",
"schedule.interval",
"scheduledTaskId",
"snoozeSchedule",
"snoozeSchedule.duration",
"snoozeSchedule.id",
"snoozeSchedule.skipRecurrences",
"tags",
"throttle",
"updatedAt",
"updatedBy"
],
"api_key_pending_invalidation": [
"apiKeyId",
"createdAt"
],
"rules-settings": [
"flapping"
],
"maintenance-window": [
"enabled",
"events"
],
"graph-workspace": [
"description",
"kibanaSavedObjectMeta",
"kibanaSavedObjectMeta.searchSourceJSON",
"legacyIndexPatternRef",
"numLinks",
"numVertices",
"title",
"version",
"wsState"
],
"search": [
"description",
"title"
],
"visualization": [
"description",
"kibanaSavedObjectMeta",
"title",
"version"
],
"canvas-element": [
"@created",
"@timestamp",
"content",
"help",
"image",
"name"
],
"canvas-workpad": [
"@created",
"@timestamp",
"name"
],
"canvas-workpad-template": [
"help",
"name",
"tags",
"template_key"
],
"event-annotation-group": [
"description",
"title"
],
"dashboard": [
"controlGroupInput",
"controlGroupInput.chainingSystem",
"controlGroupInput.controlStyle",
"controlGroupInput.ignoreParentSettingsJSON",
"controlGroupInput.panelsJSON",
"description",
"hits",
"kibanaSavedObjectMeta",
"kibanaSavedObjectMeta.searchSourceJSON",
"optionsJSON",
"panelsJSON",
"refreshInterval",
"refreshInterval.display",
"refreshInterval.pause",
"refreshInterval.section",
"refreshInterval.value",
"timeFrom",
"timeRestore",
"timeTo",
"title",
"version"
],
"links": [
"description",
"links",
"title"
],
"lens": [
"description",
"state",
"title",
"visualizationType"
],
"lens-ui-telemetry": [
"count",
"date",
"name",
"type"
],
"map": [
"bounds",
"description",
"layerListJSON",
"mapStateJSON",
"title",
"uiStateJSON",
"version"
],
"cases-comments": [
"actions",
"actions.type",
"alertId",
"comment",
"created_at",
"created_by",
"created_by.username",
"externalReferenceAttachmentTypeId",
"owner",
"persistableStateAttachmentTypeId",
"pushed_at",
"type",
"updated_at"
],
"cases-configure": [
"closure_type",
"created_at",
"owner"
],
"cases-connector-mappings": [
"owner"
],
"cases": [
"assignees",
"assignees.uid",
"category",
"closed_at",
"closed_by",
"closed_by.email",
"closed_by.full_name",
"closed_by.profile_uid",
"closed_by.username",
"connector",
"connector.fields",
"connector.fields.key",
"connector.fields.value",
"connector.name",
"connector.type",
"created_at",
"created_by",
"created_by.email",
"created_by.full_name",
"created_by.profile_uid",
"created_by.username",
"customFields",
"customFields.key",
"customFields.type",
"customFields.value",
"description",
"duration",
"external_service",
"external_service.connector_name",
"external_service.external_id",
"external_service.external_title",
"external_service.external_url",
"external_service.pushed_at",
"external_service.pushed_by",
"external_service.pushed_by.email",
"external_service.pushed_by.full_name",
"external_service.pushed_by.profile_uid",
"external_service.pushed_by.username",
"owner",
"settings",
"settings.syncAlerts",
"severity",
"status",
"tags",
"title",
"total_alerts",
"total_comments",
"updated_at",
"updated_by",
"updated_by.email",
"updated_by.full_name",
"updated_by.profile_uid",
"updated_by.username"
],
"cases-user-actions": [
"action",
"created_at",
"created_by",
"created_by.username",
"owner",
"payload",
"payload.assignees",
"payload.assignees.uid",
"payload.comment",
"payload.comment.externalReferenceAttachmentTypeId",
"payload.comment.persistableStateAttachmentTypeId",
"payload.comment.type",
"payload.connector",
"payload.connector.type",
"type"
],
"cases-telemetry": [],
"infrastructure-monitoring-log-view": [
"name"
],
"metrics-data-source": [],
"ingest_manager_settings": [
"fleet_server_hosts",
"has_seen_add_data_notice",
"prerelease_integrations_enabled",
"secret_storage_requirements_met"
],
"ingest-agent-policies": [
"agent_features",
"agent_features.enabled",
"agent_features.name",
"data_output_id",
"description",
"download_source_id",
"fleet_server_host_id",
"inactivity_timeout",
"is_default",
"is_default_fleet_server",
"is_managed",
"is_preconfigured",
"is_protected",
"keep_monitoring_alive",
"monitoring_enabled",
"monitoring_output_id",
"name",
"namespace",
"overrides",
"revision",
"schema_version",
"status",
"unenroll_timeout",
"updated_at",
"updated_by"
],
"ingest-outputs": [
"allow_edit",
"auth_type",
"broker_ack_reliability",
"broker_buffer_size",
"broker_timeout",
"ca_sha256",
"ca_trusted_fingerprint",
"channel_buffer_size",
"client_id",
"compression",
"compression_level",
"config",
"config_yaml",
"connection_type",
"hash",
"hash.hash",
"hash.random",
"headers",
"headers.key",
"headers.value",
"hosts",
"is_default",
"is_default_monitoring",
"is_preconfigured",
"key",
"name",
"output_id",
"partition",
"password",
"proxy_id",
"random",
"random.group_events",
"required_acks",
"round_robin",
"round_robin.group_events",
"sasl",
"sasl.mechanism",
"secrets",
"secrets.password",
"secrets.password.id",
"secrets.service_token",
"secrets.service_token.id",
"secrets.ssl",
"secrets.ssl.key",
"secrets.ssl.key.id",
"service_token",
"shipper",
"ssl",
"timeout",
"topics",
"topics.topic",
"topics.when",
"topics.when.condition",
"topics.when.type",
"type",
"username",
"version"
],
"ingest-package-policies": [
"created_at",
"created_by",
"description",
"elasticsearch",
"enabled",
"inputs",
"is_managed",
"name",
"namespace",
"package",
"package.name",
"package.title",
"package.version",
"policy_id",
"revision",
"secret_references",
"secret_references.id",
"updated_at",
"updated_by",
"vars"
],
"epm-packages": [
"es_index_patterns",
"experimental_data_stream_features",
"experimental_data_stream_features.data_stream",
"experimental_data_stream_features.features",
"experimental_data_stream_features.features.synthetic_source",
"experimental_data_stream_features.features.tsdb",
"install_format_schema_version",
"install_source",
"install_started_at",
"install_status",
"install_version",
"installed_es",
"installed_es.deferred",
"installed_es.id",
"installed_es.type",
"installed_es.version",
"installed_kibana",
"installed_kibana_space_id",
"internal",
"keep_policies_up_to_date",
"latest_install_failed_attempts",
"name",
"package_assets",
"verification_key_id",
"verification_status",
"version"
],
"epm-packages-assets": [
"asset_path",
"data_base64",
"data_utf8",
"install_source",
"media_type",
"package_name",
"package_version"
],
"fleet-preconfiguration-deletion-record": [
"id"
],
"ingest-download-sources": [
"host",
"is_default",
"name",
"proxy_id",
"source_id"
],
"fleet-fleet-server-host": [
"host_urls",
"is_default",
"is_preconfigured",
"name",
"proxy_id"
],
"fleet-proxy": [
"certificate",
"certificate_authorities",
"certificate_key",
"is_preconfigured",
"name",
"proxy_headers",
"url"
],
"fleet-message-signing-keys": [],
"fleet-uninstall-tokens": [
"policy_id",
"token_plain"
],
"osquery-manager-usage-metric": [
"count",
"errors"
],
"osquery-saved-query": [
"created_at",
"created_by",
"description",
"ecs_mapping",
"id",
"interval",
"platform",
"query",
"timeout",
"updated_at",
"updated_by",
"version"
],
"osquery-pack": [
"created_at",
"created_by",
"description",
"enabled",
"name",
"queries",
"queries.ecs_mapping",
"queries.id",
"queries.interval",
"queries.platform",
"queries.query",
"queries.timeout",
"queries.version",
"shards",
"updated_at",
"updated_by",
"version"
],
"osquery-pack-asset": [
"description",
"name",
"queries",
"queries.ecs_mapping",
"queries.id",
"queries.interval",
"queries.platform",
"queries.query",
"queries.timeout",
"queries.version",
"shards",
"version"
],
"csp-rule-template": [
"metadata",
"metadata.benchmark",
"metadata.benchmark.id",
"metadata.benchmark.name",
"metadata.benchmark.posture_type",
"metadata.benchmark.rule_number",
"metadata.benchmark.version",
"metadata.id",
"metadata.name",
"metadata.section",
"metadata.version"
],
"slo": [
"budgetingMethod",
"description",
"enabled",
"id",
"indicator",
"indicator.params",
"indicator.type",
"name",
"tags"
],
"threshold-explorer-view": [],
"observability-onboarding-state": [
"progress",
"state",
"type"
],
"ml-job": [
"datafeed_id",
"job_id",
"type"
],
"ml-trained-model": [
"job",
"job.create_time",
"job.job_id",
"model_id"
],
"ml-module": [
"datafeeds",
"defaultIndexPattern",
"description",
"id",
"jobs",
"logo",
"query",
"tags",
"title",
"type"
],
"uptime-dynamic-settings": [],
"synthetics-privates-locations": [],
"synthetics-monitor": [
"alert",
"alert.status",
"alert.status.enabled",
"alert.tls",
"alert.tls.enabled",
"custom_heartbeat_id",
"enabled",
"hash",
"hosts",
"id",
"journey_id",
"locations",
"locations.id",
"locations.label",
"name",
"origin",
"project_id",
"schedule",
"schedule.number",
"tags",
"throttling",
"throttling.label",
"type",
"urls"
],
"uptime-synthetics-api-key": [
"apiKey"
],
"synthetics-param": [],
"infrastructure-ui-source": [],
"inventory-view": [],
"metrics-explorer-view": [],
"upgrade-assistant-reindex-operation": [
"indexName",
"status"
],
"upgrade-assistant-ml-upgrade-operation": [
"snapshotId"
],
"monitoring-telemetry": [
"reportedClusterUuids"
],
"enterprise_search_telemetry": [],
"app_search_telemetry": [],
"workplace_search_telemetry": [],
"siem-ui-timeline-note": [
"created",
"createdBy",
"eventId",
"note",
"updated",
"updatedBy"
],
"siem-ui-timeline-pinned-event": [
"created",
"createdBy",
"eventId",
"updated",
"updatedBy"
],
"siem-detection-engine-rule-actions": [
"actions",
"actions.actionRef",
"actions.action_type_id",
"actions.group",
"actions.id",
"actions.params",
"alertThrottle",
"ruleAlertId",
"ruleThrottle"
],
"security-rule": [
"rule_id",
"version"
],
"siem-ui-timeline": [
"columns",
"columns.aggregatable",
"columns.category",
"columns.columnHeaderType",
"columns.description",
"columns.example",
"columns.id",
"columns.indexes",
"columns.name",
"columns.placeholder",
"columns.searchable",
"columns.type",
"created",
"createdBy",
"dataProviders",
"dataProviders.and",
"dataProviders.and.enabled",
"dataProviders.and.excluded",
"dataProviders.and.id",
"dataProviders.and.kqlQuery",
"dataProviders.and.name",
"dataProviders.and.queryMatch",
"dataProviders.and.queryMatch.displayField",
"dataProviders.and.queryMatch.displayValue",
"dataProviders.and.queryMatch.field",
"dataProviders.and.queryMatch.operator",
"dataProviders.and.queryMatch.value",
"dataProviders.and.type",
"dataProviders.enabled",
"dataProviders.excluded",
"dataProviders.id",
"dataProviders.kqlQuery",
"dataProviders.name",
"dataProviders.queryMatch",
"dataProviders.queryMatch.displayField",
"dataProviders.queryMatch.displayValue",
"dataProviders.queryMatch.field",
"dataProviders.queryMatch.operator",
"dataProviders.queryMatch.value",
"dataProviders.type",
"dateRange",
"dateRange.end",
"dateRange.start",
"description",
"eqlOptions",
"eqlOptions.eventCategoryField",
"eqlOptions.query",
"eqlOptions.size",
"eqlOptions.tiebreakerField",
"eqlOptions.timestampField",
"eventType",
"excludedRowRendererIds",
"favorite",
"favorite.favoriteDate",
"favorite.fullName",
"favorite.keySearch",
"favorite.userName",
"filters",
"filters.exists",
"filters.match_all",
"filters.meta",
"filters.meta.alias",
"filters.meta.controlledBy",
"filters.meta.disabled",
"filters.meta.field",
"filters.meta.formattedValue",
"filters.meta.index",
"filters.meta.key",
"filters.meta.negate",
"filters.meta.params",
"filters.meta.relation",
"filters.meta.type",
"filters.meta.value",
"filters.missing",
"filters.query",
"filters.range",
"filters.script",
"indexNames",
"kqlMode",
"kqlQuery",
"kqlQuery.filterQuery",
"kqlQuery.filterQuery.kuery",
"kqlQuery.filterQuery.kuery.expression",
"kqlQuery.filterQuery.kuery.kind",
"kqlQuery.filterQuery.serializedQuery",
"savedSearchId",
"sort",
"sort.columnId",
"sort.columnType",
"sort.sortDirection",
"status",
"templateTimelineId",
"templateTimelineVersion",
"timelineType",
"title",
"updated",
"updatedBy"
],
"endpoint:user-artifact-manifest": [
"artifacts",
"schemaVersion"
],
"security-solution-signals-migration": [
"sourceIndex",
"updated",
"version"
],
"risk-engine-configuration": [
"dataViewId",
"enabled",
"filter",
"identifierType",
"interval",
"pageSize",
"range",
"range.end",
"range.start"
],
"policy-settings-protection-updates-note": [
"note"
],
"apm-telemetry": [],
"apm-server-schema": [
"schemaJson"
],
"apm-service-group": [
"color",
"description",
"groupName",
"kuery"
],
"apm-custom-dashboards": [
"dashboardSavedObjectId",
"kuery",
"serviceEnvironmentFilterEnabled",
"serviceNameFilterEnabled"
]
}

View file

@ -11,7 +11,7 @@ import Path from 'path';
import type { SavedObjectsTypeMappingDefinitions } from '@kbn/core-saved-objects-base-server-internal';
export const CURRENT_MAPPINGS_FILE = Path.resolve(__dirname, '../current_mappings.json');
export const CURRENT_MAPPINGS_FILE = Path.resolve(__dirname, '../../current_mappings.json');
export async function readCurrentMappings(): Promise<SavedObjectsTypeMappingDefinitions> {
let currentMappingsJson;

View file

@ -8,7 +8,6 @@
import ChildProcess from 'child_process';
import { Readable } from 'stream';
import * as Rx from 'rxjs';
import { REPO_ROOT } from '@kbn/repo-info';
@ -18,19 +17,13 @@ import type { SavedObjectsTypeMappingDefinitions } from '@kbn/core-saved-objects
import type { Result } from './extract_mappings_from_plugins_worker';
function routeToLog(readable: Readable, log: SomeDevLog, level: 'debug' | 'error') {
return observeLines(readable).pipe(
Rx.tap((line) => {
log[level](line);
}),
Rx.ignoreElements()
);
}
/**
* Run a worker process that starts the core with all plugins enabled and sends back the
* saved object mappings for all plugins. We run this in a child process so that we can
* harvest logs and feed them into the logger when debugging.
* saved object mappings for all plugins.
*
* We run this in a child process to make it easier to kill the kibana instance once done
* (dodges issues with open handles), and so that we can harvest logs and feed them into
* the logger when debugging.
*/
export async function extractMappingsFromPlugins(
log: SomeDevLog
@ -78,3 +71,12 @@ export async function extractMappingsFromPlugins(
return mappings;
}
function routeToLog(readable: Readable, log: SomeDevLog, level: 'debug' | 'error') {
return observeLines(readable).pipe(
Rx.tap((line) => {
log[level](line);
}),
Rx.ignoreElements()
);
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { runMappingsCompatibilityChecks } from './run_mappings_compatibility_check';

View file

@ -0,0 +1,90 @@
/*
* 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 deepEqual from 'fast-deep-equal';
import { ToolingLog } from '@kbn/tooling-log';
import { CleanupTask } from '@kbn/dev-cli-runner';
import { createTestEsCluster } from '@kbn/test';
import { extractMappingsFromPlugins } from './extract_mappings_from_plugins';
import { checkAdditiveOnlyChange } from './check_additive_only_change';
import { checkIncompatibleMappings } from './check_incompatible_mappings';
import { readCurrentMappings, updateCurrentMappings } from './current_mappings';
export const runMappingsCompatibilityChecks = async ({
fix,
verify,
log,
addCleanupTask,
}: {
fix: boolean;
verify: boolean;
log: ToolingLog;
addCleanupTask: (task: CleanupTask) => void;
}) => {
/**
* Algorithm for checking compatible mappings. Should work in CI or local
* dev environment.
* 1. Extract mappings from code as JSON object
* 2. Check if extracted mappings is different from current_mappings.json, current_mappings.json stores
* the mappings from upstream and is commited to each branch
* 3. Start a fresh ES node
* 4. Upload current_mappings.json to ES node
* 5. Upload extracted mappings.json to ES node
* 6. Check result of response to step 5, if bad response the mappings are incompatible
* 7. If good response, write extracted mappings to current_mappings.json
*/
log.info('Extracting mappings from plugins');
const extractedMappings = await log.indent(4, async () => {
return await extractMappingsFromPlugins(log);
});
const currentMappings = await readCurrentMappings();
const isMappingChanged = !deepEqual(currentMappings, extractedMappings);
if (!isMappingChanged) {
log.success('Mappings are unchanged.');
return;
}
if (verify) {
log.info('Checking if any mappings have been removed');
await log.indent(4, async () => {
return checkAdditiveOnlyChange(log, currentMappings, extractedMappings);
});
log.info('Starting es...');
const esClient = await log.indent(4, async () => {
const cluster = createTestEsCluster({ log });
await cluster.start();
addCleanupTask(() => cluster.cleanup());
return cluster.getClient();
});
log.info(`Checking if mappings are compatible`);
await log.indent(4, async () => {
await checkIncompatibleMappings({
log,
esClient,
currentMappings,
nextMappings: extractedMappings,
});
});
}
if (fix) {
await updateCurrentMappings(extractedMappings);
log.warning(
`Updated extracted mappings in current_mappings.json file, please commit the changes if desired.`
);
} else {
log.warning(
`The extracted mappings do not match the current_mappings.json file, run with --fix to update.`
);
}
};

View file

@ -0,0 +1,65 @@
/*
* 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 { difference } from 'lodash';
export interface CompareResult {
error: boolean;
fieldsToAdd: string[];
registeredFields: string[];
missingFromModelVersion: string[];
missingFromDefinition: string[];
}
export const compareFieldLists = ({
currentFields,
registeredFields = [],
modelVersionFields = [],
}: {
currentFields: string[] | undefined;
registeredFields: string[] | undefined;
modelVersionFields: string[] | undefined;
}): CompareResult => {
// type not present in the file, so it was just added.
// in that case we just update the file to add all the registered fields.
if (!currentFields) {
return {
error: false,
registeredFields,
fieldsToAdd: registeredFields,
missingFromModelVersion: [],
missingFromDefinition: [],
};
}
// we search all registered/mv fields not already in the file
const registeredFieldsNotInCurrent = difference(registeredFields, currentFields);
const modelVersionFieldsNotInCurrent = difference(modelVersionFields, currentFields);
// then we search for registered fields not in model versions, and the opposite
const registeredFieldsNotInModelVersions = difference(
registeredFieldsNotInCurrent,
modelVersionFieldsNotInCurrent
);
const modelVersionFieldsNotRegistered = difference(
modelVersionFieldsNotInCurrent,
registeredFieldsNotInCurrent
);
// if any non-file field is present only in mapping definition or in model version, then there's an error on the type
const anyFieldMissing =
registeredFieldsNotInModelVersions.length > 0 || modelVersionFieldsNotRegistered.length > 0;
return {
error: anyFieldMissing,
registeredFields,
fieldsToAdd: registeredFieldsNotInCurrent,
missingFromModelVersion: registeredFieldsNotInModelVersions,
missingFromDefinition: modelVersionFieldsNotRegistered,
};
};

View file

@ -0,0 +1,29 @@
/*
* 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 { readFile, writeFile } from 'fs/promises';
import Path from 'path';
import { FieldListMap } from '@kbn/core-saved-objects-base-server-internal';
const CURRENT_FIELDS_FILE_PATH = Path.resolve(__dirname, '../../current_fields.json');
export const readCurrentFields = async (): Promise<FieldListMap> => {
try {
const fileContent = await readFile(CURRENT_FIELDS_FILE_PATH, 'utf-8');
return JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
return {};
}
throw error;
}
};
export const writeCurrentFields = async (fieldMap: FieldListMap) => {
await writeFile(CURRENT_FIELDS_FILE_PATH, JSON.stringify(fieldMap, null, 2) + '\n', 'utf-8');
};

View file

@ -0,0 +1,75 @@
/*
* 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 ChildProcess from 'child_process';
import { Readable } from 'stream';
import * as Rx from 'rxjs';
import { REPO_ROOT } from '@kbn/repo-info';
import { SomeDevLog } from '@kbn/some-dev-log';
import { observeLines } from '@kbn/stdio-dev-helpers';
import type { Result } from './extract_field_lists_from_plugins_worker';
/**
* Run a worker process that starts the core with all plugins enabled and sends back the
* registered fields for all plugins.
*
* We run this in a child process to make it easier to kill the kibana instance once done
* (dodges issues with open handles), and so that we can harvest logs and feed them into
* the logger when debugging.
*/
export async function extractFieldListsFromPlugins(log: SomeDevLog): Promise<Result> {
log.info('Loading core with all plugins enabled so that we can get all savedObject mappings...');
const fork = ChildProcess.fork(require.resolve('./extract_field_lists_from_plugins_worker.ts'), {
execArgv: ['--require=@kbn/babel-register/install'],
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
});
const result = await Rx.firstValueFrom(
Rx.merge(
// the actual value we are interested in
Rx.fromEvent(fork, 'message'),
// worker logs are written to the logger, but dropped from the stream
routeToLog(fork.stdout!, log, 'debug'),
routeToLog(fork.stderr!, log, 'error'),
// if an error occurs running the worker throw it into the stream
Rx.fromEvent(fork, 'error').pipe(
Rx.map((err) => {
throw err;
})
)
).pipe(
Rx.takeUntil(Rx.fromEvent(fork, 'exit')),
Rx.map((results) => {
const [outcome] = results as [Result];
log.debug('message received from worker', outcome);
fork.kill('SIGILL');
return outcome;
}),
Rx.defaultIfEmpty(undefined)
)
);
if (!result) {
throw new Error('worker exited without sending mappings');
}
return result;
}
function routeToLog(readable: Readable, log: SomeDevLog, level: 'debug' | 'error') {
return observeLines(readable).pipe(
Rx.tap((line) => {
log[level](line);
}),
Rx.ignoreElements()
);
}

View file

@ -0,0 +1,75 @@
/*
* 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 { createRootWithCorePlugins } from '@kbn/core-test-helpers-kbn-server';
import { set } from '@kbn/safer-lodash-set';
import { PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH } from '@kbn/core-plugins-server-internal/src/constants';
import {
FieldListMap,
getFieldListMapFromMappingDefinitions,
SavedObjectsTypeMappingDefinitions,
} from '@kbn/core-saved-objects-base-server-internal';
import { getFieldListMapFromModelVersions } from './get_field_list_from_model_version';
export interface Result {
fieldsFromRegisteredTypes: FieldListMap;
fieldsFromModelVersions: FieldListMap;
}
(async () => {
if (!process.send) {
throw new Error('worker must be run in a node.js fork');
}
const settings = {
logging: {
loggers: [{ name: 'root', level: 'info', appenders: ['console'] }],
},
migrations: { skip: true },
elasticsearch: { skipStartupConnectionCheck: true },
};
set(settings, PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH, true);
const root = createRootWithCorePlugins(settings, {
basePath: false,
cache: false,
dev: true,
disableOptimizer: true,
silent: false,
dist: false,
oss: false,
runExamples: false,
watch: false,
});
await root.preboot();
const { savedObjects } = await root.setup();
const typeRegistry = savedObjects.getTypeRegistry();
const registeredTypes = typeRegistry.getAllTypes();
const registeredMappings = registeredTypes.reduce<SavedObjectsTypeMappingDefinitions>(
(memo, type) => {
memo[type.name] = type.mappings;
return memo;
},
{}
);
const fieldsFromRegisteredTypes = getFieldListMapFromMappingDefinitions(registeredMappings);
const fieldsFromModelVersions = getFieldListMapFromModelVersions(registeredTypes);
const result: Result = {
fieldsFromRegisteredTypes,
fieldsFromModelVersions,
};
process.send(result);
})().catch((error) => {
process.stderr.write(`UNHANDLED ERROR: ${error.stack}`);
process.exit(1);
});

View file

@ -0,0 +1,30 @@
/*
* 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 { SavedObjectsType } from '@kbn/core-saved-objects-server';
import { FieldListMap, getVersionAddedFields } from '@kbn/core-saved-objects-base-server-internal';
const getModelVersionAddedFieldsForType = (typeDef: SavedObjectsType): string[] => {
const addedFieldSet = new Set<string>();
const versions =
typeof typeDef.modelVersions === 'function'
? typeDef.modelVersions()
: typeDef.modelVersions ?? {};
Object.values(versions).forEach((version) => {
const addedFields = getVersionAddedFields(version);
addedFields.forEach((field) => addedFieldSet.add(field));
});
return [...addedFieldSet].sort();
};
export const getFieldListMapFromModelVersions = (types: SavedObjectsType[]): FieldListMap => {
return types.reduce<FieldListMap>((memo, type) => {
memo[type.name] = getModelVersionAddedFieldsForType(type);
return memo;
}, {});
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { runModelVersionMappingAdditionsChecks } from './run_versions_mapping_additions_check';

View file

@ -0,0 +1,111 @@
/*
* 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 { ToolingLog } from '@kbn/tooling-log';
import { createFailError } from '@kbn/dev-cli-errors';
import { FieldListMap } from '@kbn/core-saved-objects-base-server-internal';
import { compareFieldLists, type CompareResult } from './compare_type_field_lists';
import { readCurrentFields, writeCurrentFields } from './current_fields';
import { extractFieldListsFromPlugins } from './extract_field_lists_from_plugins';
export const runModelVersionMappingAdditionsChecks = async ({
fix,
override,
verify,
log,
}: {
fix: boolean;
override: boolean;
verify: boolean;
log: ToolingLog;
}) => {
log.info('Generating field lists from registry and file');
const { fieldsFromRegisteredTypes, fieldsFromModelVersions } = await extractFieldListsFromPlugins(
log
);
const currentFields = await readCurrentFields();
const allTypeNames = [
...new Set([
...Object.keys(fieldsFromRegisteredTypes),
...Object.keys(currentFields),
...Object.keys(fieldsFromModelVersions),
]),
];
log.info('Generating field delta');
const results = allTypeNames.reduce<Record<string, CompareResult>>((memo, typeName) => {
memo[typeName] = compareFieldLists({
registeredFields: fieldsFromRegisteredTypes[typeName],
currentFields: currentFields[typeName],
modelVersionFields: fieldsFromModelVersions[typeName],
});
return memo;
}, {});
const hasError = Object.values(results).some((result) => result.error);
if (hasError) {
const errorMessage = getErrorMessage(results);
if (verify) {
throw createFailError(errorMessage + `\nUse --override --no-verify`);
} else {
log.warning(errorMessage);
}
}
if (fix || override) {
log.info(`Updating field file with override: ${override}`);
const updatedFields = updateCurrentFields(currentFields, results, override);
await writeCurrentFields(updatedFields);
}
};
const getErrorMessage = (results: Record<string, CompareResult>): string => {
const errors = Object.entries(results)
.filter(([_, result]) => result.error)
.reduce<string[]>((memo, [typeName, result]) => {
if (result.missingFromDefinition.length) {
memo.push(
`- ${typeName}: found mappings from model version not present in mappings definition: ${result.missingFromDefinition.join(
','
)}`
);
}
if (result.missingFromModelVersion.length) {
memo.push(
`- ${typeName}: found mappings from root definition not present in any model version: ${result.missingFromModelVersion.join(
','
)}`
);
}
return memo;
}, []);
return `Found issues in savedObjects mappings:\n${errors.join('\n')}`;
};
const updateCurrentFields = (
currentFields: FieldListMap,
results: Record<string, CompareResult>,
override: boolean
): FieldListMap => {
// mutating the field lists is fine
const updatedFields = override ? {} : { ...currentFields };
Object.entries(results).forEach(([typeName, typeResult]) => {
if (override) {
updatedFields[typeName] = [...typeResult.registeredFields].sort();
} else {
if (!typeResult.error) {
updatedFields[typeName] = [
...new Set([...(updatedFields[typeName] || []), ...typeResult.fieldsToAdd]),
].sort();
}
}
});
return updatedFields;
};

View file

@ -6,95 +6,46 @@
* Side Public License, v 1.
*/
import deepEqual from 'fast-deep-equal';
import { run } from '@kbn/dev-cli-runner';
import { createTestEsCluster } from '@kbn/test';
import { extractMappingsFromPlugins } from './extract_mappings_from_plugins';
import { checkAdditiveOnlyChange } from './check_additive_only_change';
import { checkIncompatibleMappings } from './check_incompatible_mappings';
import { readCurrentMappings, updateCurrentMappings } from './current_mappings';
import { runMappingsCompatibilityChecks } from './compatibility';
import { runModelVersionMappingAdditionsChecks } from './mappings_additions';
run(
async ({ log, flagsReader, addCleanupTask }) => {
const fix = flagsReader.boolean('fix');
const verify = flagsReader.boolean('verify');
const override = flagsReader.boolean('override');
const task = flagsReader.string('task');
/**
* Algorithm for checking compatible mappings. Should work in CI or local
* dev environment.
* 1. Extract mappings from code as JSON object
* 2. Check if extracted mappings is different from current_mappings.json, current_mappings.json stores
* the mappings from upstream and is commited to each branch
* 3. Start a fresh ES node
* 4. Upload current_mappings.json to ES node
* 5. Upload extracted mappings.json to ES node
* 6. Check result of response to step 5, if bad response the mappings are incompatible
* 7. If good response, write extracted mappings to current_mappings.json
*/
log.info('Extracting mappings from plugins');
const extractedMappings = await log.indent(4, async () => {
return await extractMappingsFromPlugins(log);
});
const currentMappings = await readCurrentMappings();
const isMappingChanged = !deepEqual(currentMappings, extractedMappings);
if (!isMappingChanged) {
log.success('Mappings are unchanged.');
return;
}
if (verify) {
log.info('Checking if any mappings have been removed');
if (!task || task === 'mapping-addition') {
log.info('Running model version mapping addition checks');
await log.indent(4, async () => {
return checkAdditiveOnlyChange(log, currentMappings, extractedMappings);
});
log.info('Starting es...');
const esClient = await log.indent(4, async () => {
const cluster = createTestEsCluster({ log });
await cluster.start();
addCleanupTask(() => cluster.cleanup());
return cluster.getClient();
});
log.info(`Checking if mappings are compatible`);
await log.indent(4, async () => {
await checkIncompatibleMappings({
log,
esClient,
currentMappings,
nextMappings: extractedMappings,
});
await runModelVersionMappingAdditionsChecks({ fix, override, verify, log });
});
}
if (fix) {
await updateCurrentMappings(extractedMappings);
log.warning(
`Updated extracted mappings in current_mappings.json file, please commit the changes if desired.`
);
} else {
log.warning(
`The extracted mappings do not match the current_mappings.json file, run with --fix to update.`
);
if (!task || task === 'compatibility') {
log.info('Running mapping compatibility checks');
await log.indent(4, async () => {
await runMappingsCompatibilityChecks({ fix, verify, log, addCleanupTask });
});
}
},
{
description: `
Determine if the current SavedObject mappings in the source code can be applied to the current mappings from upstream.
Determine if the changes performed to the savedObjects mappings are following our standards
`,
flags: {
boolean: ['fix', 'verify'],
boolean: ['fix', 'override', 'verify'],
string: ['task'],
default: {
verify: true,
mappings: true,
},
help: `
--fix If the current mappings differ from the mappings in the file, update the current_mappings.json file
--override If the current mappings differ from the mappings in the file, update the current_mappings.json file
--no-verify Don't run any validation, just update the current_mappings.json file.
--task Specify which task(s) to run (compatibility | mapping-addition)
`,
},
}

View file

@ -26,5 +26,7 @@
"@kbn/test",
"@kbn/core-elasticsearch-client-server-mocks",
"@kbn/safer-lodash-set",
"@kbn/tooling-log",
"@kbn/core-saved-objects-server",
]
}

View file

@ -106,7 +106,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"infrastructure-ui-source": "113182d6895764378dfe7fa9fa027244f3a457c4",
"ingest-agent-policies": "7633e578f60c074f8267bc50ec4763845e431437",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "4dd3cb38a91c848df95336a24a5abde2c8560fd1",
"ingest-outputs": "20bd44ce6016079c3b28f1b2bc241e7715be48f8",
"ingest-package-policies": "f4c2767e852b700a8b82678925b86bac08958b43",
"ingest_manager_settings": "64955ef1b7a9ffa894d4bb9cf863b5602bfa6885",
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",

View file

@ -310,6 +310,25 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
},
],
},
'3': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
secrets: {
properties: {
service_token: {
dynamic: false,
properties: {
id: { type: 'keyword' },
},
},
},
},
},
},
],
},
},
migrations: {
'7.13.0': migrateOutputToV7130,