Skip to content

Commit 70bca10

Browse files
Handling removed typed automatically (elastic#241880)
Resolves [issue](elastic/kibana-team#1936) ## Summary This PR updates our CI checks for Saved Object (SO) types to automatically handle removed types. The removed type is detected and then handled by adding to our list of `removed_types` and also prevents future re-registration of the same type name. This helps in reducing the need for manual reviews in PRs that remove types. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent c464d7f commit 70bca10

15 files changed

Lines changed: 333 additions & 5 deletions

File tree

dev_docs/tutorials/saved_objects.mdx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,78 @@ export const foo: SavedObjectsType = {
355355

356356
[2] Set this to `true` to build your own HTTP API and have complete control over the route handler.
357357

358+
## Removing a Saved Object type
359+
360+
When you need to remove a Saved Object type from your plugin, there are important steps to follow to ensure the type name cannot be accidentally reused in the future.
361+
362+
### Why do we track removed types?
363+
364+
Once a Saved Object type has been registered and used in production, its name becomes "reserved" for that purpose. If we allowed the same name to be reused for a different type later:
365+
366+
- **Migration conflicts**: Old documents with the removed type name could interfere with new documents using the same name
367+
- **Upgrade failures**: Users upgrading from older versions could experience migration failures if type names are reused
368+
369+
To prevent these issues, Kibana maintains a list of all removed type names in `packages/kbn-check-saved-objects-cli/removed_types.json`. This ensures type names are never reused.
370+
371+
### How to remove a Saved Object type
372+
373+
#### Step 1: Remove the type registration
374+
375+
First, remove the type registration from your plugin. Also remove the type definition file and any references to it.
376+
377+
#### Step 2: Run the automated check
378+
379+
Kibana includes an automated check that detects when types are removed. Run it locally to update the `removed_types.json` file:
380+
381+
```bash
382+
# Get your current commit ID
383+
git log -n 1
384+
385+
# Get the merge-base commit with main
386+
git merge-base -a <currentCommitSha> main
387+
388+
# Run the check with the baseline
389+
node scripts/check_saved_objects --baseline <mergeBase> --fix
390+
```
391+
392+
Replace `<currentCommitSha>` with your actual commit SHA from the first command, and `<mergeBase>` with the merge-base commit SHA from the second command.
393+
394+
This command will:
395+
1. Compare the current registered types against the baseline (merge-base with main)
396+
2. Detect any types that were removed
397+
3. Automatically add the removed type name(s) to `removed_types.json`
398+
4. Exit with an error message indicating which types were added
399+
400+
The error message will look like:
401+
402+
```
403+
❌ The following SO types are no longer registered: 'my-removed-type'.
404+
Updated 'removed_types.json' to prevent the same names from being reused in the future.
405+
```
406+
407+
#### Step 3: Commit the changes
408+
409+
Include both your code changes and the updated `removed_types.json` file in your commit.
410+
411+
### Manual update (alternative)
412+
413+
If you prefer to update `removed_types.json` manually, you can:
414+
415+
1. Open `packages/kbn-check-saved-objects-cli/removed_types.json`
416+
2. Add your removed type name to the array in alphabetical order
417+
3. Save the file
418+
419+
### Important considerations
420+
421+
<DocCallOut color="warning" title="Type names cannot be reused">
422+
Once a type name is added to `removed_types.json`, it cannot be used again for a new Saved Object type. Choose type names carefully when creating new types.
423+
</DocCallOut>
424+
425+
Before removing a type, ensure:
426+
- **No references exist**: Check that no other Saved Object types reference the type you're removing
427+
- **Data migration is complete**: If users have documents of this type, ensure they've been migrated to a new type or are no longer needed
428+
- **Coordinate with stakeholders**: Confirm with your team and any dependent teams that the removal is expected
429+
358430
## Troubleshooting
359431

360432
### CI is failing for my PR
@@ -406,3 +478,15 @@ These errors are pretty obvious. Model versions must be defined as consecutive n
406478
* The current serverless release. If a high-severity issue occurs, Serverless might be rolled back to a previous version. This could in turn impact CI pipelines for PRs that would otherwise be fine. In other words, _"from the current serverless standpoint, your PR is not releasable at the moment"_ . This should only happen exceptionally. Please take a look at the `#kibana-mission-control` channel to see if something is going on.
407479
* **Scenario 2.1**: Kibana has been rolled back. Please wait until the situation goes back to normal, i.e. an emergency release is performed after the rollback, and Kibana gets to a normal release state.
408480
* **Scenario 2.2**: Kibana has NOT been rolled back. Reach out to `#kibana-core` and we will help you figure out what's going on.
481+
482+
```
483+
❌ The following SO types are no longer registered: '<soType>'. Please run with --fix to update 'removed_types.json'.
484+
```
485+
**Problem:** You removed a Saved Object type but didn't update the `removed_types.json` file to track the removal.
486+
**Solution:** Run `node scripts/check_saved_objects --baseline <mergeBase> --fix` locally to automatically update the `removed_types.json` file, then commit the changes. See the <DocLink id="kibDevTutorialSavedObject" section="removing-a-saved-object-type" text="Removing a Saved Object type"/> section for detailed instructions on how to determine the correct merge-base commit.
487+
488+
```
489+
❌ Cannot re-register previously removed type(s): <soType>. Please use a different name.
490+
```
491+
**Problem:** You're trying to create a new Saved Object type using a name that was previously used and removed. Type names cannot be reused to prevent migration conflicts.
492+
**Solution:** Choose a different name for your Saved Object type. Once a type name is added to `packages/kbn-check-saved-objects-cli/removed_types.json`, it's permanently reserved and cannot be reused.

packages/kbn-check-saved-objects-cli/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99

1010
export { runCheckSavedObjectsCli } from './src/commands/run_check_saved_objects_cli';
1111
export { runCheckMappingsUpdateCli } from './src/commands/run_check_mappings_update_cli';
12+
export { getRemovedTypes } from './src/migrations/removed_types/get_removed_types';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
"apm-services-telemetry",
3+
"application_usage_transactional",
4+
"background-session",
5+
"cases-sub-case",
6+
"csp_rule",
7+
"endpoint:user-artifact",
8+
"file-upload-telemetry",
9+
"fleet-agent-actions",
10+
"fleet-agent-events",
11+
"fleet-agents",
12+
"fleet-enrollment-api-keys",
13+
"guided-onboarding-guide-state",
14+
"guided-onboarding-plugin-state",
15+
"guided-setup-state",
16+
"investigation",
17+
"maps-telemetry",
18+
"ml-telemetry",
19+
"osquery-usage-metric",
20+
"server",
21+
"siem-detection-engine-rule-execution-info",
22+
"siem-detection-engine-rule-status",
23+
"timelion-sheet",
24+
"tsvb-validation-telemetry",
25+
"ui-counter",
26+
"upgrade-assistant-telemetry"
27+
]

packages/kbn-check-saved-objects-cli/src/commands/run_check_saved_objects_cli.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { startServers, stopServers } from '../util/servers';
1313
import type { TaskContext } from './types';
1414
import {
1515
automatedRollbackTests,
16+
checkRemovedTypes,
1617
getSnapshots,
1718
validateNewTypes,
1819
validateUpdatedTypes,
@@ -37,6 +38,8 @@ export function runCheckSavedObjectsCli() {
3738
gitRev,
3839
newTypes: [],
3940
updatedTypes: [],
41+
currentRemovedTypes: [],
42+
newRemovedTypes: [],
4043
fixtures: {},
4144
fix,
4245
};
@@ -51,6 +54,10 @@ export function runCheckSavedObjectsCli() {
5154
title: 'Get type registry snapshots',
5255
task: getSnapshots,
5356
},
57+
{
58+
title: 'Check removed SO types',
59+
task: checkRemovedTypes,
60+
},
5461
{
5562
title: 'Validate new SO types',
5663
task: validateNewTypes,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { ListrTask } from 'listr2';
11+
import type { Task, TaskContext } from '../types';
12+
import {
13+
detectConflictsWithRemovedTypes,
14+
detectNewRemovedTypes,
15+
getRemovedTypes,
16+
updateRemovedTypes,
17+
} from '../../migrations/removed_types';
18+
19+
export const checkRemovedTypes: Task = async (ctx, task) => {
20+
const subtasks: ListrTask<TaskContext>[] = [
21+
{
22+
title: 'Detecting conflicts with removed types',
23+
task: async () => {
24+
ctx.currentRemovedTypes = await getRemovedTypes();
25+
await detectConflictsWithRemovedTypes(ctx.to!, ctx.currentRemovedTypes);
26+
},
27+
},
28+
{
29+
title: `Detecting new removed types`,
30+
task: () => {
31+
ctx.newRemovedTypes = detectNewRemovedTypes(ctx.from!, ctx.to!, ctx.currentRemovedTypes);
32+
},
33+
},
34+
{
35+
title: `Updating removed types`,
36+
task: async () => {
37+
await updateRemovedTypes(ctx.newRemovedTypes, ctx.currentRemovedTypes, ctx.fix);
38+
},
39+
skip: () => ctx.newRemovedTypes.length === 0,
40+
},
41+
];
42+
43+
return task.newListr<TaskContext>(subtasks);
44+
};

packages/kbn-check-saved-objects-cli/src/commands/tasks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { getSnapshots } from './get_snapshots';
1111
export { validateNewTypes } from './validate_new_types';
1212
export { validateUpdatedTypes } from './validate_updated_types';
1313
export { automatedRollbackTests } from './automated_rollback_tests';
14+
export { checkRemovedTypes } from './check_removed_types';

packages/kbn-check-saved-objects-cli/src/commands/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface TaskContext {
2020
to?: MigrationSnapshot;
2121
newTypes: string[];
2222
updatedTypes: string[];
23+
currentRemovedTypes: string[];
24+
newRemovedTypes: string[];
2325
fixtures: Record<
2426
string,
2527
{

packages/kbn-check-saved-objects-cli/src/compatibility/extract_mappings_from_plugins_worker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { set } from '@kbn/safer-lodash-set';
1212
import { buildTypesMappings } from '@kbn/core-saved-objects-migration-server-internal';
1313
import type { SavedObjectsTypeMappingDefinitions } from '@kbn/core-saved-objects-base-server-internal';
1414
import { PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH } from '@kbn/core-plugins-server-internal/src/constants';
15-
import { REMOVED_TYPES } from '@kbn/core-saved-objects-server-internal';
15+
import { getRemovedTypes } from '../..';
1616

1717
export interface Result {
1818
mappings: SavedObjectsTypeMappingDefinitions;
@@ -59,7 +59,8 @@ const cleanupRemovedTypes = (
5959
await root.preboot();
6060
const { savedObjects } = await root.setup();
6161
const mappings = buildTypesMappings(savedObjects.getTypeRegistry().getAllTypes());
62-
cleanupRemovedTypes(mappings, REMOVED_TYPES);
62+
const removedTypes = await getRemovedTypes();
63+
cleanupRemovedTypes(mappings, removedTypes);
6364
const result: Result = { mappings };
6465
process.send(result);
6566
})().catch((error) => {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { resolve } from 'path';
11+
12+
export const REMOVED_TYPES_JSON_PATH = resolve(__dirname, '../../../removed_types.json');
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { MigrationSnapshot } from '../../types';
11+
12+
/**
13+
* Detects new types in 'to' that conflict with previously removed types
14+
*/
15+
export async function detectConflictsWithRemovedTypes(
16+
to: MigrationSnapshot,
17+
currentRemovedTypes: string[]
18+
) {
19+
const toTypes = Object.keys(to.typeDefinitions);
20+
21+
const conflictingTypes: string[] = [];
22+
23+
for (const type of toTypes) {
24+
if (currentRemovedTypes.includes(type)) {
25+
conflictingTypes.push(type);
26+
}
27+
}
28+
29+
if (conflictingTypes.length > 0) {
30+
throw new Error(
31+
`❌ Cannot re-register previously removed type(s): ${conflictingTypes.join(
32+
', '
33+
)}. Please use a different name.`
34+
);
35+
}
36+
}

0 commit comments

Comments
 (0)