Skip to content

Commit 3f6472d

Browse files
Introduce "Feature States" for managing snapshots of system indices (#63513)
This PR expands the meaning of `include_global_state` for snapshots to include system indices. If `include_global_state` is `true` on creation, system indices will be included in the snapshot regardless of the contents of the `indices` field. If `include_global_state` is `true` on restoration, system indices will be restored (if included in the snapshot), regardless of the contents of the `indices` field. Index renaming is not applied to system indices, as system indices rely on their names matching certain patterns. If restored system indices are already present, they are automatically deleted prior to restoration from the snapshot to avoid conflicts. This behavior can be overridden to an extent by including a new field in the snapshot creation or restoration call, `feature_states`, which contains an array of strings indicating the "feature" for which system indices should be snapshotted or restored. For example, this call will only restore the `watcher` and `security` system indices (in addition to `index_1`): ``` POST /_snapshot/my_repository/snapshot_2/_restore { "indices": "index_1", "include_global_state": true, "feature_states": ["watcher", "security"] } ``` If `feature_states` is present, the system indices associated with those features will be snapshotted or restored regardless of the value of `include_global_state`. All system indices can be omitted by providing a special value of `none` (`"feature_states": ["none"]`), or included by omitting the field or explicitly providing an empty array (`"feature_states": []`), similar to the `indices` field. The list of currently available features can be retrieved via a new "Get Snapshottable Features" API: ``` GET /_snapshottable_features ``` which returns a response of the form: ``` { "features": [ { "name": "tasks", "description": "Manages task results" }, { "name": "kibana", "description": "Manages Kibana configuration and reports" } ] } ``` Features currently map one-to-one with `SystemIndexPlugin`s, but this should be considered an implementation detail. The Get Snapshottable Features API and snapshot creation rely upon all relevant plugins being installed on the master node. Further, the list of feature states included in a given snapshot is exposed by the Get Snapshot API, which now includes a new field, `feature_states`, which contains a list of the feature states and their associated system indices which are included in the snapshot. All system indices in feature states are also included in the `indices` array for backwards compatibility, although explicitly requesting system indices included in a feature state is deprecated. For example, an excerpt from the Get Snapshot API showing `feature_states`: ``` "feature_states": [ { "feature_name": "tasks", "indices": [ ".tasks" ] } ], "indices": [ ".tasks", "test1", "test2" ] ``` Co-authored-by: William Brafford <[email protected]>
1 parent 293fcd4 commit 3f6472d

File tree

101 files changed

+3175
-293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+3175
-293
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
2929
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
3030
import org.elasticsearch.action.support.master.AcknowledgedResponse;
31+
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
32+
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesResponse;
3133

3234
import java.io.IOException;
3335

@@ -378,4 +380,47 @@ public Cancellable deleteAsync(DeleteSnapshotRequest deleteSnapshotRequest, Requ
378380
SnapshotRequestConverters::deleteSnapshot, options,
379381
AcknowledgedResponse::fromXContent, listener, emptySet());
380382
}
383+
384+
/**
385+
* Get a list of features which can be included in a snapshot as feature states.
386+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/get-snapshottable-features-api.html"> Get Snapshottable
387+
* Features API on elastic.co</a>
388+
*
389+
* @param getFeaturesRequest the request
390+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
391+
* @return the response
392+
* @throws IOException in case there is a problem sending the request or parsing back the response
393+
*/
394+
public GetSnapshottableFeaturesResponse getFeatures(GetSnapshottableFeaturesRequest getFeaturesRequest, RequestOptions options)
395+
throws IOException {
396+
return restHighLevelClient.performRequestAndParseEntity(
397+
getFeaturesRequest,
398+
SnapshotRequestConverters::getSnapshottableFeatures,
399+
options,
400+
GetSnapshottableFeaturesResponse::parse,
401+
emptySet()
402+
);
403+
}
404+
405+
/**
406+
* Asynchronously get a list of features which can be included in a snapshot as feature states.
407+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/get-snapshottable-features-api.html"> Get Snapshottable
408+
* Features API on elastic.co</a>
409+
*
410+
* @param getFeaturesRequest the request
411+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
412+
* @param listener the listener to be notified upon request completion
413+
* @return cancellable that may be used to cancel the request
414+
*/
415+
public Cancellable getFeaturesAsync(GetSnapshottableFeaturesRequest getFeaturesRequest, RequestOptions options,
416+
ActionListener<GetSnapshottableFeaturesResponse> listener) {
417+
return restHighLevelClient.performRequestAsyncAndParseEntity(
418+
getFeaturesRequest,
419+
SnapshotRequestConverters::getSnapshottableFeatures,
420+
options,
421+
GetSnapshottableFeaturesResponse::parse,
422+
listener,
423+
emptySet()
424+
);
425+
}
381426
}

client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
2424
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
2525
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
26+
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
2627
import org.elasticsearch.common.Strings;
2728

2829
import java.io.IOException;
@@ -190,4 +191,13 @@ static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) {
190191
request.addParameters(parameters.asMap());
191192
return request;
192193
}
194+
195+
static Request getSnapshottableFeatures(GetSnapshottableFeaturesRequest getSnapshottableFeaturesRequest) {
196+
String endpoint = "/_snapshottable_features";
197+
Request request = new Request(HttpGet.METHOD_NAME, endpoint);
198+
RequestConverters.Params parameters = new RequestConverters.Params();
199+
parameters.withMasterTimeout(getSnapshottableFeaturesRequest.masterNodeTimeout());
200+
request.addParameters(parameters.asMap());
201+
return request;
202+
}
193203
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.snapshots;
10+
11+
import org.elasticsearch.client.TimedRequest;
12+
13+
/**
14+
* A {@link TimedRequest} to get the list of features available to be included in snapshots in the cluster.
15+
*/
16+
public class GetSnapshottableFeaturesRequest extends TimedRequest {
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.snapshots;
10+
11+
import org.elasticsearch.common.ParseField;
12+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
13+
import org.elasticsearch.common.xcontent.ObjectParser;
14+
import org.elasticsearch.common.xcontent.XContentParser;
15+
16+
import java.util.List;
17+
import java.util.Objects;
18+
19+
public class GetSnapshottableFeaturesResponse {
20+
21+
private final List<SnapshottableFeature> features;
22+
23+
private static final ParseField FEATURES = new ParseField("features");
24+
25+
@SuppressWarnings("unchecked")
26+
private static final ConstructingObjectParser<GetSnapshottableFeaturesResponse, Void> PARSER = new ConstructingObjectParser<>(
27+
"snapshottable_features_response", true, (a, ctx) -> new GetSnapshottableFeaturesResponse((List<SnapshottableFeature>) a[0])
28+
);
29+
30+
static {
31+
PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), SnapshottableFeature::parse, FEATURES);
32+
}
33+
34+
public GetSnapshottableFeaturesResponse(List<SnapshottableFeature> features) {
35+
this.features = features;
36+
}
37+
38+
public List<SnapshottableFeature> getFeatures() {
39+
return features;
40+
}
41+
42+
public static GetSnapshottableFeaturesResponse parse(XContentParser parser) {
43+
return PARSER.apply(parser, null);
44+
}
45+
46+
@Override
47+
public boolean equals(Object o) {
48+
if (this == o) return true;
49+
if ((o instanceof GetSnapshottableFeaturesResponse) == false) return false;
50+
GetSnapshottableFeaturesResponse that = (GetSnapshottableFeaturesResponse) o;
51+
return getFeatures().equals(that.getFeatures());
52+
}
53+
54+
@Override
55+
public int hashCode() {
56+
return Objects.hash(getFeatures());
57+
}
58+
59+
public static class SnapshottableFeature {
60+
61+
private final String featureName;
62+
private final String description;
63+
64+
private static final ParseField FEATURE_NAME = new ParseField("name");
65+
private static final ParseField DESCRIPTION = new ParseField("description");
66+
67+
private static final ConstructingObjectParser<SnapshottableFeature, Void> PARSER = new ConstructingObjectParser<>(
68+
"feature", true, (a, ctx) -> new SnapshottableFeature((String) a[0], (String) a[1])
69+
);
70+
71+
static {
72+
PARSER.declareField(ConstructingObjectParser.constructorArg(),
73+
(p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING);
74+
PARSER.declareField(ConstructingObjectParser.constructorArg(),
75+
(p, c) -> p.text(), DESCRIPTION, ObjectParser.ValueType.STRING);
76+
}
77+
78+
public SnapshottableFeature(String featureName, String description) {
79+
this.featureName = featureName;
80+
this.description = description;
81+
}
82+
83+
public static SnapshottableFeature parse(XContentParser parser, Void ctx) {
84+
return PARSER.apply(parser, ctx);
85+
}
86+
87+
public String getFeatureName() {
88+
return featureName;
89+
}
90+
91+
public String getDescription() {
92+
return description;
93+
}
94+
95+
@Override
96+
public boolean equals(Object o) {
97+
if (this == o) return true;
98+
if ((o instanceof SnapshottableFeature) == false) return false;
99+
SnapshottableFeature feature = (SnapshottableFeature) o;
100+
return Objects.equals(getFeatureName(), feature.getFeatureName());
101+
}
102+
103+
@Override
104+
public int hashCode() {
105+
return Objects.hash(getFeatureName());
106+
}
107+
}
108+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
2929
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
3030
import org.elasticsearch.action.support.master.AcknowledgedResponse;
31+
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
32+
import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesResponse;
3133
import org.elasticsearch.cluster.metadata.IndexMetadata;
3234
import org.elasticsearch.common.settings.Settings;
3335
import org.elasticsearch.common.xcontent.XContentType;
@@ -39,12 +41,16 @@
3941
import java.io.IOException;
4042
import java.util.Collections;
4143
import java.util.HashMap;
44+
import java.util.List;
4245
import java.util.Map;
4346

47+
import static org.elasticsearch.snapshots.SnapshotsService.NO_FEATURE_STATES_VALUE;
48+
import static org.elasticsearch.tasks.TaskResultsService.TASKS_FEATURE_NAME;
4449
import static org.hamcrest.Matchers.equalTo;
4550
import static org.hamcrest.Matchers.greaterThan;
4651
import static org.hamcrest.Matchers.hasSize;
4752
import static org.hamcrest.Matchers.is;
53+
import static org.hamcrest.Matchers.notNullValue;
4854

4955
public class SnapshotIT extends ESRestHighLevelClientTestCase {
5056

@@ -150,6 +156,14 @@ public void testCreateSnapshot() throws Exception {
150156
}
151157
request.partial(randomBoolean());
152158
request.includeGlobalState(randomBoolean());
159+
final List<String> featureStates = randomFrom(
160+
List.of(
161+
Collections.emptyList(),
162+
Collections.singletonList(TASKS_FEATURE_NAME),
163+
Collections.singletonList(NO_FEATURE_STATES_VALUE)
164+
)
165+
);
166+
request.featureStates(featureStates);
153167

154168
CreateSnapshotResponse response = createTestSnapshot(request);
155169
assertEquals(waitForCompletion ? RestStatus.OK : RestStatus.ACCEPTED, response.status());
@@ -262,9 +276,14 @@ public void testRestoreSnapshot() throws IOException {
262276
assertFalse("index [" + testIndex + "] should have been deleted", indexExists(testIndex));
263277

264278
RestoreSnapshotRequest request = new RestoreSnapshotRequest(testRepository, testSnapshot);
279+
request.indices(testIndex);
265280
request.waitForCompletion(true);
266281
request.renamePattern(testIndex);
267282
request.renameReplacement(restoredIndex);
283+
if (randomBoolean()) {
284+
request.includeGlobalState(true);
285+
request.featureStates(Collections.singletonList(NO_FEATURE_STATES_VALUE));
286+
}
268287

269288
RestoreSnapshotResponse response = execute(request, highLevelClient().snapshot()::restore,
270289
highLevelClient().snapshot()::restoreAsync);
@@ -364,6 +383,18 @@ public void testCloneSnapshot() throws IOException {
364383
assertTrue(response.isAcknowledged());
365384
}
366385

386+
public void testGetFeatures() throws IOException {
387+
GetSnapshottableFeaturesRequest request = new GetSnapshottableFeaturesRequest();
388+
389+
GetSnapshottableFeaturesResponse response = execute(request,
390+
highLevelClient().snapshot()::getFeatures, highLevelClient().snapshot()::getFeaturesAsync);
391+
392+
assertThat(response, notNullValue());
393+
assertThat(response.getFeatures(), notNullValue());
394+
assertThat(response.getFeatures().size(), greaterThan(1));
395+
assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName())));
396+
}
397+
367398
private static Map<String, Object> randomUserMetadata() {
368399
if (randomBoolean()) {
369400
return null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.client.snapshots;
10+
11+
import org.elasticsearch.client.AbstractResponseTestCase;
12+
import org.elasticsearch.common.xcontent.XContentParser;
13+
import org.elasticsearch.common.xcontent.XContentType;
14+
15+
import java.io.IOException;
16+
import java.util.Map;
17+
import java.util.stream.Collectors;
18+
19+
import static org.hamcrest.Matchers.everyItem;
20+
import static org.hamcrest.Matchers.hasSize;
21+
import static org.hamcrest.Matchers.in;
22+
import static org.hamcrest.Matchers.is;
23+
24+
public class GetSnapshottableFeaturesResponseTests extends AbstractResponseTestCase<
25+
org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse,
26+
GetSnapshottableFeaturesResponse> {
27+
28+
@Override
29+
protected org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse createServerTestInstance(
30+
XContentType xContentType
31+
) {
32+
return new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse(
33+
randomList(
34+
10,
35+
() -> new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse.SnapshottableFeature(
36+
randomAlphaOfLengthBetween(4, 10),
37+
randomAlphaOfLengthBetween(5, 10)
38+
)
39+
)
40+
);
41+
}
42+
43+
@Override
44+
protected GetSnapshottableFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException {
45+
return GetSnapshottableFeaturesResponse.parse(parser);
46+
}
47+
48+
@Override
49+
protected void assertInstances(
50+
org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse serverTestInstance,
51+
GetSnapshottableFeaturesResponse clientInstance
52+
) {
53+
assertNotNull(serverTestInstance.getSnapshottableFeatures());
54+
assertNotNull(serverTestInstance.getSnapshottableFeatures());
55+
56+
assertThat(clientInstance.getFeatures(), hasSize(serverTestInstance.getSnapshottableFeatures().size()));
57+
58+
Map<String, String> clientFeatures = clientInstance.getFeatures()
59+
.stream()
60+
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription()));
61+
Map<String, String> serverFeatures = serverTestInstance.getSnapshottableFeatures()
62+
.stream()
63+
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription()));
64+
65+
assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet()))));
66+
}
67+
}

docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,35 @@ argument is provided, the snapshot only includes the specified data streams and
9999
+
100100
--
101101
(Optional, Boolean)
102-
If `true`, the current cluster state is included in the snapshot.
102+
If `true`, the current global state is included in the snapshot.
103103
Defaults to `true`.
104104

105-
The cluster state includes:
105+
The global state includes:
106106

107107
* Persistent cluster settings
108108
* Index templates
109109
* Legacy index templates
110110
* Ingest pipelines
111111
* {ilm-init} lifecycle policies
112+
* Data stored in system indices, such as Watches and task records (configurable via `feature_states`)
112113
--
113114
+
114115
IMPORTANT: By default, the entire snapshot will fail if one or more indices included in the snapshot do not have all primary shards available. You can change this behavior by setting <<create-snapshot-api-partial,`partial`>> to `true`.
115116

117+
[[create-snapshot-api-feature-states]]
118+
`feature_states`::
119+
(Optional, array of strings)
120+
A list of feature states to be included in this snapshot. A list of features
121+
available for inclusion in the snapshot and their descriptions be can be
122+
retrieved using the <<get-snapshottable-features-api,get snapshottable features API>>.
123+
Each feature state includes one or more system indices containing data necessary
124+
for the function of that feature. Providing an empty array will include no feature
125+
states in the snapshot, regardless of the value of `include_global_state`.
126+
+
127+
By default, all available feature states will be included in the snapshot if
128+
`include_global_state` is `true`, or no feature states if `include_global_state`
129+
is `false`.
130+
116131
include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=master-timeout]
117132

118133
`metadata`::
@@ -163,6 +178,7 @@ The API returns the following response:
163178
"version": <version>,
164179
"indices": [],
165180
"data_streams": [],
181+
"feature_states": [],
166182
"include_global_state": false,
167183
"metadata": {
168184
"taken_by": "user123",

0 commit comments

Comments
 (0)