Skip to content

fix(search): guard active-index deletion and gate ES/OS delete cascade (#35640)#36399

Open
fabrizzio-dotCMS wants to merge 2 commits into
mainfrom
issue-35640-index-delete-guard-and-cascade-ff
Open

fix(search): guard active-index deletion and gate ES/OS delete cascade (#35640)#36399
fabrizzio-dotCMS wants to merge 2 commits into
mainfrom
issue-35640-index-delete-guard-and-cascade-ff

Conversation

@fabrizzio-dotCMS

@fabrizzio-dotCMS fabrizzio-dotCMS commented Jul 1, 2026

Copy link
Copy Markdown
Member

Problem

QA on #35640 (epic #35476, TC-016–020) surfaced defects in the index delete path (DELETE /api/v1/esindex/{name}ContentletIndexAPIImpl.delete()):

  • TC-018 (High): deleting the currently active index was possible via REST/AJAX. The maintenance UI only hides the Delete option for active indices (a client-side guard), so a direct DELETE /api/v1/esindex/{name} bypassed it and could leave the site with zero indices.
  • TC-016: the open design question of whether delete should cascade to the .os twin — undocumented and not configurable.
  • TC-016/017: the endpoint returned a false 404 when given the full physical name (with cluster prefix), returned an empty 404 body, and had an inverted-sense helper (indexExists returned true when the index did not exist).

Changes

TC-018 — active-index guard (ContentletIndexAPIImpl.delete)

  • Rejects deletion of any index reported active/building by the phase-aware getCurrentIndex()getNewIndex() — the same sources the maintenance UI uses to flag active/building — via DotStateException, mapped to HTTP 400 in ESIndexResource.
  • Fail-closed if the active set can't be resolved.
  • Bypass: FEATURE_FLAG_ALLOW_ACTIVE_INDEX_DELETE (default false).
  • Reuses the phase-aware getters rather than VersionedIndices directly, because VersionedIndices is empty in Phase 0 (rows are only written once migration starts), which would leave the most common prod state unprotected.

TC-016 — cascade feature flag (ContentletIndexAPIImpl.delete)

TC-016/017 — cleanups (ESIndexResource)

  • Normalize the incoming name with removeClusterIdFromName in both deleteIndex and modIndex, so the endpoints accept both the short name and the full physical name (no more false 404).
  • Readable 404 body instead of an empty response.
  • Rename indexExistsindexDoesNotExist (matches its actual meaning).

Testing

  • ContentletIndexAPIImplTest#delete_activeIndex_isRejected_unlessFeatureFlagOverrides — active index survives a rejected delete; FF bypass then allows it.
  • ContentletIndexAPIImplMigrationIntegrationTest#test_delete_phase1_cascadeOff_removesOnlyNamedEngine — Phase 1, cascade OFF: bare name deletes ES only, .os twin survives (sibling to the existing cascade-ON test).
  • Core compiles clean; integration module test-compiles clean; no openapi.yaml diff.

Out of scope (follow-ups)

  • Cascade for site-search indices (separate subsystem, deferred in the migration doc).
  • UI confirmation dialog on delete (requested by @jcastro-dotcms).

🤖 Generated with Claude Code

@mergify

mergify Bot commented Jul 1, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @fabrizzio-dotCMS's task in 3m 4s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🤖 dotBot Review (Bedrock)

Reviewed 4 file(s); 3 candidate(s) → 2 confirmed, 0 uncertain (unverified, kept for review).

Confirmed findings

  • 🟠 High dotCMS/src/main/java/com/dotcms/rest/api/v1/index/ESIndexResource.java:568 — 404 response in modIndex lacks error message body
    The modIndex method throws NotFoundInDbException which returns an empty 404 response body, while deleteIndex uses ResponseUtil.mapErrorResponse() to return structured JSON errors. This inconsistency breaks client error handling expectations.
  • 🟡 Medium dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplMigrationIntegrationTest.java:462 — Test uses bare index name without cluster prefix
    The test checks for OpenSearch index 'DUAL_WORKING' but physical indices include cluster ID prefixes. This could cause false negatives in index existence checks during cascade-off testing, as the actual OS index name would be prefixed with the cluster ID.

us.deepseek.r1-v1:0 · Run: #28604656874 · tokens: in: 30329 · out: 10353 · total: 40682 · calls: 11 · est. ~$0.097

fabrizzio-dotCMS and others added 2 commits July 2, 2026 10:12
#35640)

Index delete had two defects surfaced by QA (epic #35476, TC-016..020):

TC-018 — deleting the currently active index was possible via the REST/AJAX
endpoint. The maintenance UI only hides the Delete option for active indices
(a client-side guard), so a direct DELETE /api/v1/esindex/{name} bypassed it and
could leave the site with zero indices. Add a server-side guard in
ContentletIndexAPIImpl.delete(): reject deletion of any index reported active or
building by the phase-aware getCurrentIndex()/getNewIndex() (the same sources the
UI uses), via DotStateException. ESIndexResource maps it to HTTP 400. The guard
is fail-closed and can be bypassed with FEATURE_FLAG_ALLOW_ACTIVE_INDEX_DELETE.
Guard reuses the phase-aware getters rather than VersionedIndices directly
because VersionedIndices is empty in Phase 0 (rows are only written once
migration starts), which would leave the most common prod state unprotected.

TC-016 — delete cascades to the .os twin (phase-dispatched, since #35820). Make
this an explicit, documented default gated by FEATURE_FLAG_INDEX_DELETE_CASCADE
(default true). When off, delete is tag-dispatched (name ending in .os -> OS,
otherwise ES) so only the named engine's index is removed and the twin is left
intact — the ticket's documented no-cascade behavior.

TC-016/017 cleanups in ESIndexResource: normalize the incoming name with
removeClusterIdFromName so the endpoint accepts both the short name and the full
physical name (with cluster prefix) instead of 404-ing on the latter; return a
readable 404 body instead of an empty response; rename the inverted-sense
indexExists helper to indexDoesNotExist.

Tests: ContentletIndexAPIImplTest#delete_activeIndex_isRejected_unlessFeatureFlagOverrides
and ContentletIndexAPIImplMigrationIntegrationTest#test_delete_phase1_cascadeOff_removesOnlyNamedEngine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5640)

Apply the same removeClusterIdFromName normalization to the modIndex
(activate/deactivate/clear/open/close) endpoint that deleteIndex already uses,
so both accept the short name and the full physical name (with cluster prefix)
instead of 404-ing on the latter. Addresses the AI review consistency finding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fabrizzio-dotCMS fabrizzio-dotCMS force-pushed the issue-35640-index-delete-guard-and-cascade-ff branch from 73fa8a3 to 3311769 Compare July 2, 2026 16:12
@fabrizzio-dotCMS fabrizzio-dotCMS changed the base branch from issue-36396-esraw-phase-aware to main July 2, 2026 16:12
// latter (issue #35640, TC-016).
final String resolvedName = indexAPI.removeClusterIdFromName(indexName);

if(indexDoesNotExist(resolvedName) ){

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [High] 404 response in modIndex lacks error message body

The modIndex method throws NotFoundInDbException which returns an empty 404 response body, while deleteIndex uses ResponseUtil.mapErrorResponse() to return structured JSON errors. This inconsistency breaks client error handling expectations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant