Skip to content

Commit a9c15a6

Browse files
authored
[7.x] Disallow ILM searchable snapshot actions that use different repositories (#68856) (#68866)
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 cbbd6a0 commit a9c15a6

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;
@@ -598,6 +599,40 @@ public void testShouldMigrateDataToTiers() {
598599
}
599600
}
600601

602+
public void testValidatingSearchableSnapshotRepos() {
603+
Map<String, LifecycleAction> hotActions = new HashMap<>();
604+
Map<String, LifecycleAction> coldActions = new HashMap<>();
605+
Map<String, LifecycleAction> frozenActions = new HashMap<>();
606+
607+
{
608+
hotActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
609+
coldActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
610+
frozenActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
611+
612+
Phase hotPhase = new Phase(HOT_PHASE, TimeValue.ZERO, hotActions);
613+
Phase coldPhase = new Phase(HOT_PHASE, TimeValue.ZERO, coldActions);
614+
Phase frozenPhase = new Phase(HOT_PHASE, TimeValue.ZERO, frozenActions);
615+
616+
validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase));
617+
}
618+
619+
{
620+
hotActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
621+
coldActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo2", randomBoolean(), null));
622+
frozenActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean(), null));
623+
624+
Phase hotPhase = new Phase(HOT_PHASE, TimeValue.ZERO, hotActions);
625+
Phase coldPhase = new Phase(HOT_PHASE, TimeValue.ZERO, coldActions);
626+
Phase frozenPhase = new Phase(HOT_PHASE, TimeValue.ZERO, frozenActions);
627+
628+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
629+
() -> validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase)));
630+
assertThat(e.getMessage(), containsString("policy specifies [searchable_snapshot]" +
631+
" action multiple times with differing repositories [repo2, repo1]," +
632+
" the same repository must be used for all searchable snapshot actions"));
633+
}
634+
}
635+
601636
private void assertNextActionName(String phaseName, String currentAction, String expectedNextAction, String... availableActionNames) {
602637
Map<String, LifecycleAction> availableActions = convertActionNamesToActions(availableActionNames);
603638
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
@@ -57,6 +57,7 @@
5757
import static org.elasticsearch.xpack.TimeSeriesRestDriver.getStepKeyForIndex;
5858
import static org.elasticsearch.xpack.TimeSeriesRestDriver.indexDocument;
5959
import static org.elasticsearch.xpack.TimeSeriesRestDriver.rolloverMaxOneDocCondition;
60+
import static org.hamcrest.Matchers.containsString;
6061
import static org.hamcrest.Matchers.equalTo;
6162
import static org.hamcrest.Matchers.greaterThan;
6263
import static org.hamcrest.Matchers.is;
@@ -378,6 +379,7 @@ public void testIdenticalSearchableSnapshotActionIsNoop() throws Exception {
378379
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
379380
.build());
380381
ensureGreen(index);
382+
indexDocument(client(), index, true);
381383

382384
final String searchableSnapMountedIndexName = (storage == MountSearchableSnapshotRequest.Storage.FULL_COPY ?
383385
SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX : SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX) + index;
@@ -401,6 +403,10 @@ public void testIdenticalSearchableSnapshotActionIsNoop() throws Exception {
401403
}
402404
assertThat("expected to have only one snapshot, but got: " + responseMap,
403405
((List<Object>) responseMap.get("snapshots")).size(), equalTo(1));
406+
407+
Request hitCount = new Request("GET", "/" + searchableSnapMountedIndexName + "/_count");
408+
Map<String, Object> count = entityAsMap(client().performRequest(hitCount));
409+
assertThat("expected a single document but got: " + count, (int) count.get("count"), equalTo(1));
404410
}
405411

406412
@SuppressWarnings("unchecked")
@@ -421,7 +427,7 @@ public void testConvertingSearchableSnapshotFromFullToPartial() throws Exception
421427
.put(LifecycleSettings.LIFECYCLE_NAME, policy)
422428
.build());
423429
ensureGreen(index);
424-
indexDocument(client(), index);
430+
indexDocument(client(), index, true);
425431

426432
final String searchableSnapMountedIndexName = SearchableSnapshotAction.PARTIAL_RESTORED_INDEX_PREFIX +
427433
SearchableSnapshotAction.FULL_RESTORED_INDEX_PREFIX + index;
@@ -445,6 +451,10 @@ public void testConvertingSearchableSnapshotFromFullToPartial() throws Exception
445451
}
446452
assertThat("expected to have only one snapshot, but got: " + responseMap,
447453
((List<Object>) responseMap.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;
@@ -489,71 +499,28 @@ public void testConvertingPartialSearchableSnapshotIntoFull() throws Exception {
489499
}
490500
assertThat("expected to have only one snapshot, but got: " + responseMap,
491501
((List<Object>) responseMap.get("snapshots")).size(), equalTo(1));
502+
503+
Request hitCount = new Request("GET", "/" + searchableSnapMountedIndexName + "/_count");
504+
Map<String, Object> count = entityAsMap(client().performRequest(hitCount));
505+
assertThat("expected a single document but got: " + count, (int) count.get("count"), equalTo(1));
492506
}
493507

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

0 commit comments

Comments
 (0)