Skip to content

Commit 7f412ee

Browse files
authored
Disallow ILM searchable snapshot actions that use different repositories (#68856)
This commit adds validation when updating or creating an ILM policy that mulitple searchable snapshot actions all use the same repository. We currently do not have the infrastructure to support switching a searchable snapshot from one repository to another, so until we have that we will disallow this (edge case) when creating a policy. Relates to #68714
1 parent 608d358 commit 7f412ee

File tree

5 files changed

+88
-64
lines changed

5 files changed

+88
-64
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ public MountSearchableSnapshotRequest.Storage getStorageType() {
117117
return storageType;
118118
}
119119

120+
public String getSnapshotRepository() {
121+
return snapshotRepository;
122+
}
123+
120124
@Override
121125
public List<Step> toSteps(Client client, String phase, StepKey nextStepKey) {
122126
StepKey preActionBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_ACTION_STEP);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ public void validate(Collection<Phase> phases) {
290290
}
291291

292292
validateActionsFollowingSearchableSnapshot(phases);
293+
validateAllSearchableSnapshotActionsUseSameRepository(phases);
293294
}
294295

295296
static void validateActionsFollowingSearchableSnapshot(Collection<Phase> phases) {
@@ -313,6 +314,22 @@ static void validateActionsFollowingSearchableSnapshot(Collection<Phase> phases)
313314
}
314315
}
315316

317+
static void validateAllSearchableSnapshotActionsUseSameRepository(Collection<Phase> phases) {
318+
Set<String> allRepos = phases.stream()
319+
.flatMap(phase -> phase.getActions().entrySet().stream())
320+
.filter(e -> e.getKey().equals(SearchableSnapshotAction.NAME))
321+
.map(Map.Entry::getValue)
322+
.map(action -> (SearchableSnapshotAction) action)
323+
.map(SearchableSnapshotAction::getSnapshotRepository)
324+
.collect(Collectors.toSet());
325+
326+
if (allRepos.size() > 1) {
327+
throw new IllegalArgumentException("policy specifies [" + SearchableSnapshotAction.NAME +
328+
"] action multiple times with differing repositories " + allRepos +
329+
", the same repository must be used for all searchable snapshot actions");
330+
}
331+
}
332+
316333
private static boolean definesAllocationRules(AllocateAction action) {
317334
return action.getRequire().isEmpty() == false || action.getInclude().isEmpty() == false || action.getExclude().isEmpty() == false;
318335
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.Set;
3131
import java.util.function.Function;
3232

33+
import static org.elasticsearch.xpack.core.ilm.SearchableSnapshotActionTests.randomStorageType;
3334
import static org.hamcrest.Matchers.equalTo;
3435
import static org.hamcrest.Matchers.instanceOf;
3536
import static org.mockito.Mockito.mock;
@@ -207,7 +208,7 @@ private static Function<String, LifecycleAction> getNameToActionFunction() {
207208
case UnfollowAction.NAME:
208209
return new UnfollowAction();
209210
case SearchableSnapshotAction.NAME:
210-
return new SearchableSnapshotAction(randomAlphaOfLengthBetween(1, 10));
211+
return new SearchableSnapshotAction("repo", randomBoolean(), randomStorageType());
211212
case MigrateAction.NAME:
212213
return new MigrateAction(false);
213214
case RollupILMAction.NAME:

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.VALID_PHASES;
4040
import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.VALID_WARM_ACTIONS;
4141
import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.WARM_PHASE;
42+
import static org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType.validateAllSearchableSnapshotActionsUseSameRepository;
4243
import static org.hamcrest.Matchers.containsInAnyOrder;
4344
import static org.hamcrest.Matchers.containsString;
4445
import static org.hamcrest.Matchers.equalTo;
@@ -592,6 +593,40 @@ public void testShouldMigrateDataToTiers() {
592593
}
593594
}
594595

596+
public void testValidatingSearchableSnapshotRepos() {
597+
Map<String, LifecycleAction> hotActions = new HashMap<>();
598+
Map<String, LifecycleAction> coldActions = new HashMap<>();
599+
Map<String, LifecycleAction> frozenActions = new HashMap<>();
600+
601+
{
602+
hotActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
603+
coldActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
604+
frozenActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
605+
606+
Phase hotPhase = new Phase(HOT_PHASE, TimeValue.ZERO, hotActions);
607+
Phase coldPhase = new Phase(HOT_PHASE, TimeValue.ZERO, coldActions);
608+
Phase frozenPhase = new Phase(HOT_PHASE, TimeValue.ZERO, frozenActions);
609+
610+
validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase));
611+
}
612+
613+
{
614+
hotActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
615+
coldActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo2", randomBoolean(), null));
616+
frozenActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
617+
618+
Phase hotPhase = new Phase(HOT_PHASE, TimeValue.ZERO, hotActions);
619+
Phase coldPhase = new Phase(HOT_PHASE, TimeValue.ZERO, coldActions);
620+
Phase frozenPhase = new Phase(HOT_PHASE, TimeValue.ZERO, frozenActions);
621+
622+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
623+
() -> validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase)));
624+
assertThat(e.getMessage(), containsString("policy specifies [searchable_snapshot]" +
625+
" action multiple times with differing repositories [repo2, repo1]," +
626+
" the same repository must be used for all searchable snapshot actions"));
627+
}
628+
}
629+
595630
private void assertNextActionName(String phaseName, String currentAction, String expectedNextAction, String... availableActionNames) {
596631
Map<String, LifecycleAction> availableActions = convertActionNamesToActions(availableActionNames);
597632
Phase phase = new Phase(phaseName, TimeValue.ZERO, availableActions);

x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java

Lines changed: 30 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import static org.elasticsearch.xpack.TimeSeriesRestDriver.getStepKeyForIndex;
5757
import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument;
5858
import static org.elasticsearch.xpack.TimeSeriesRestDriver.rolloverMaxOneDocCondition;
59+
import static org.hamcrest.Matchers.containsString;
5960
import static org.hamcrest.Matchers.equalTo;
6061
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
6162
import static org.hamcrest.Matchers.is;
@@ -374,6 +375,7 @@ public void testIdenticalSearchableSnapshotActionIsNoop() throws Exception {
374375
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
375376
.build());
376377
ensureGreen(index);
378+
indexDocument(client(), index, true);
377379

378380
final String searchableSnapMountedIndexName = (storage == MountSearchableSnapshotRequest.Storage.FULL_COPY ?
379381
SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX : SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX) + index;
@@ -399,6 +401,10 @@ public void testIdenticalSearchableSnapshotActionIsNoop() throws Exception {
399401
((List<Map<String, Object>>)
400402
((Map<String, Object>)
401403
((List<Object>) responseMap.get("responses")).get(0)).get("snapshots")).size(), equalTo(1));
404+
405+
Request hitCount = new Request("GET", "/" + searchableSnapMountedIndexName + "/_count");
406+
Map<String, Object> count = entityAsMap(client().performRequest(hitCount));
407+
assertThat("expected a single document but got: " + count, (int) count.get("count"), equalTo(1));
402408
}
403409

404410
@SuppressWarnings("unchecked")
@@ -419,7 +425,7 @@ public void testConvertingSearchableSnapshotFromFullToPartial() throws Exception
419425
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
420426
.build());
421427
ensureGreen(index);
422-
indexDocument(client(), index);
428+
indexDocument(client(), index, true);
423429

424430
final String searchableSnapMountedIndexName = SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX +
425431
SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX + index;
@@ -445,6 +451,10 @@ public void testConvertingSearchableSnapshotFromFullToPartial() throws Exception
445451
((List<Map<String, Object>>)
446452
((Map<String, Object>)
447453
((List<Object>) responseMap.get("responses")).get(0)).get("snapshots")).size(), equalTo(1));
454+
455+
Request hitCount = new Request("GET", "/" + searchableSnapMountedIndexName + "/_count");
456+
Map<String, Object> count = entityAsMap(client().performRequest(hitCount));
457+
assertThat("expected a single document but got: " + count, (int) count.get("count"), equalTo(1));
448458
}
449459

450460
@SuppressWarnings("unchecked")
@@ -465,7 +475,7 @@ public void testConvertingPartialSearchableSnapshotIntoFull() throws Exception {
465475
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
466476
.build());
467477
ensureGreen(index);
468-
indexDocument(client(), index);
478+
indexDocument(client(), index, true);
469479

470480
final String searchableSnapMountedIndexName = SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX +
471481
SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX + index;
@@ -491,71 +501,28 @@ public void testConvertingPartialSearchableSnapshotIntoFull() throws Exception {
491501
((List<Map<String, Object>>)
492502
((Map<String, Object>)
493503
((List<Object>) responseMap.get("responses")).get(0)).get("snapshots")).size(), equalTo(1));
504+
505+
Request hitCount = new Request("GET", "/" + searchableSnapMountedIndexName + "/_count");
506+
Map<String, Object> count = entityAsMap(client().performRequest(hitCount));
507+
assertThat("expected a single document but got: " + count, (int) count.get("count"), equalTo(1));
494508
}
495509

496-
@SuppressWarnings("unchecked")
497-
@AwaitsFix(bugUrl = "functionality not yet implemented")
498-
public void testSecondSearchableSnapshotChangesRepo() throws Exception {
499-
String index = "myindex-" + randomAlphaOfLength(4).toLowerCase(Locale.ROOT);
510+
public void testSecondSearchableSnapshotUsingDifferentRepoThrows() throws Exception {
500511
String secondRepo = randomAlphaOfLengthBetween(10, 20);
501512
createSnapshotRepo(client(), snapshotRepo, randomBoolean());
502513
createSnapshotRepo(client(), secondRepo, randomBoolean());
503-
createPolicy(client(), policy, null, null,
504-
new Phase("cold", TimeValue.ZERO,
505-
singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean(),
506-
MountSearchableSnapshotRequest.Storage.FULL_COPY))),
507-
new Phase("frozen", TimeValue.ZERO,
508-
singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(secondRepo, randomBoolean(),
509-
MountSearchableSnapshotRequest.Storage.SHARED_CACHE))),
510-
null
511-
);
512-
513-
createIndex(index, Settings.builder()
514-
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
515-
.build());
516-
ensureGreen(index);
517-
indexDocument(client(), index);
518-
519-
final String searchableSnapMountedIndexName = SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX +
520-
SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX + index;
521-
522-
assertBusy(() -> {
523-
logger.info("--> waiting for [{}] to exist...", searchableSnapMountedIndexName);
524-
assertTrue(indexExists(searchableSnapMountedIndexName));
525-
}, 30, TimeUnit.SECONDS);
526-
527-
assertBusy(() -> {
528-
Step.StepKey stepKeyForIndex = getStepKeyForIndex(client(), searchableSnapMountedIndexName);
529-
assertThat(stepKeyForIndex.getPhase(), is("frozen"));
530-
assertThat(stepKeyForIndex.getName(), is(PhaseCompleteStep.NAME));
531-
}, 30, TimeUnit.SECONDS);
532-
533-
// Check first repo has exactly 1 snapshot
534-
{
535-
Request getSnaps = new Request("GET", "/_snapshot/" + snapshotRepo + "/_all");
536-
Response response = client().performRequest(getSnaps);
537-
Map<String, Object> responseMap;
538-
try (InputStream is = response.getEntity().getContent()) {
539-
responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true);
540-
}
541-
assertThat("expected to have only one snapshot, but got: " + responseMap,
542-
((List<Map<String, Object>>)
543-
((Map<String, Object>)
544-
((List<Object>) responseMap.get("responses")).get(0)).get("snapshots")).size(), equalTo(1));
545-
}
546-
547-
// Check second repo has exactly 1 snapshot
548-
{
549-
Request getSnaps = new Request("GET", "/_snapshot/" + secondRepo + "/_all");
550-
Response response = client().performRequest(getSnaps);
551-
Map<String, Object> responseMap;
552-
try (InputStream is = response.getEntity().getContent()) {
553-
responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true);
554-
}
555-
assertThat("expected to have only one snapshot, but got: " + responseMap,
556-
((List<Map<String, Object>>)
557-
((Map<String, Object>)
558-
((List<Object>) responseMap.get("responses")).get(0)).get("snapshots")).size(), equalTo(1));
559-
}
514+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
515+
createPolicy(client(), policy, null, null,
516+
new Phase("cold", TimeValue.ZERO,
517+
singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean(),
518+
MountSearchableSnapshotRequest.Storage.FULL_COPY))),
519+
new Phase("frozen", TimeValue.ZERO,
520+
singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(secondRepo, randomBoolean(),
521+
MountSearchableSnapshotRequest.Storage.SHARED_CACHE))),
522+
null
523+
));
524+
525+
assertThat(e.getMessage(),
526+
containsString("policy specifies [searchable_snapshot] action multiple times with differing repositories"));
560527
}
561528
}

0 commit comments

Comments
 (0)