Skip to content

Commit 3f8908d

Browse files
committed
Add license checks for auto-follow implementation (#33496)
This commit adds license checks for the auto-follow implementation. We check the license on put auto-follow patterns, and then for every coordination round we check that the local and remote clusters are licensed for CCR. In the case of non-compliance, we skip coordination yet continue to schedule follow-ups.
1 parent 4787055 commit 3f8908d

File tree

12 files changed

+348
-97
lines changed

12 files changed

+348
-97
lines changed

test/framework/src/main/java/org/elasticsearch/test/MockLogAppender.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public AbstractEventExpectation(String name, String logger, Level level, String
8585

8686
@Override
8787
public void match(LogEvent event) {
88-
if (event.getLevel().equals(level) && event.getLoggerName().equals(logger)) {
88+
if (event.getLevel().equals(level) && event.getLoggerName().equals(logger) && innerMatch(event)) {
8989
if (Regex.isSimpleMatchPattern(message)) {
9090
if (Regex.simpleMatch(message, event.getMessage().getFormattedMessage())) {
9191
saw = true;
@@ -97,6 +97,11 @@ public void match(LogEvent event) {
9797
}
9898
}
9999
}
100+
101+
public boolean innerMatch(final LogEvent event) {
102+
return true;
103+
}
104+
100105
}
101106

102107
public static class UnseenEventExpectation extends AbstractEventExpectation {
@@ -123,6 +128,32 @@ public void assertMatched() {
123128
}
124129
}
125130

131+
public static class ExceptionSeenEventExpectation extends SeenEventExpectation {
132+
133+
private final Class<? extends Exception> clazz;
134+
private final String exceptionMessage;
135+
136+
public ExceptionSeenEventExpectation(
137+
final String name,
138+
final String logger,
139+
final Level level,
140+
final String message,
141+
final Class<? extends Exception> clazz,
142+
final String exceptionMessage) {
143+
super(name, logger, level, message);
144+
this.clazz = clazz;
145+
this.exceptionMessage = exceptionMessage;
146+
}
147+
148+
@Override
149+
public boolean innerMatch(final LogEvent event) {
150+
return event.getThrown() != null
151+
&& event.getThrown().getClass() == clazz
152+
&& event.getThrown().getMessage().equals(exceptionMessage);
153+
}
154+
155+
}
156+
126157
public static class PatternSeenEventExcpectation implements LoggingExpectation {
127158

128159
protected final String name;

x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle renamed to x-pack/plugin/ccr/qa/multi-cluster-with-non-compliant-license/build.gradle

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,20 @@ leaderClusterTestRunner {
2020
systemProperty 'tests.is_leader_cluster', 'true'
2121
}
2222

23+
task writeJavaPolicy {
24+
doLast {
25+
final File javaPolicy = file("${buildDir}/tmp/java.policy")
26+
javaPolicy.write(
27+
[
28+
"grant {",
29+
" permission java.io.FilePermission \"${-> followClusterTest.getNodes().get(0).homeDir}/logs/${-> followClusterTest.getNodes().get(0).clusterName}.log\", \"read\";",
30+
"};"
31+
].join("\n"))
32+
}
33+
}
34+
2335
task followClusterTest(type: RestIntegTestTask) {}
36+
followClusterTest.dependsOn writeJavaPolicy
2437

2538
followClusterTestCluster {
2639
dependsOn leaderClusterTestRunner
@@ -31,8 +44,10 @@ followClusterTestCluster {
3144
}
3245

3346
followClusterTestRunner {
47+
systemProperty 'java.security.policy', "file://${buildDir}/tmp/java.policy"
3448
systemProperty 'tests.is_leader_cluster', 'false'
3549
systemProperty 'tests.leader_host', "${-> leaderClusterTest.nodes.get(0).httpUri()}"
50+
systemProperty 'log', "${-> followClusterTest.getNodes().get(0).homeDir}/logs/${-> followClusterTest.getNodes().get(0).clusterName}.log"
3651
finalizedBy 'leaderClusterTestCluster#stop'
3752
}
3853

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
import org.elasticsearch.client.Request;
1010
import org.elasticsearch.client.ResponseException;
1111
import org.elasticsearch.common.Booleans;
12+
import org.elasticsearch.common.io.PathUtils;
1213
import org.elasticsearch.test.rest.ESRestTestCase;
1314

15+
import java.nio.file.Files;
16+
import java.util.Iterator;
17+
import java.util.List;
1418
import java.util.Locale;
1519

1620
import static org.hamcrest.Matchers.containsString;
21+
import static org.hamcrest.Matchers.equalTo;
1722
import static org.hamcrest.Matchers.hasToString;
1823

1924
public class CcrMultiClusterLicenseIT extends ESRestTestCase {
@@ -29,19 +34,52 @@ public void testFollowIndex() {
2934
if (runningAgainstLeaderCluster == false) {
3035
final Request request = new Request("POST", "/follower/_ccr/follow");
3136
request.setJsonEntity("{\"leader_index\": \"leader_cluster:leader\"}");
32-
assertLicenseIncompatible(request);
37+
assertNonCompliantLicense(request);
3338
}
3439
}
3540

3641
public void testCreateAndFollowIndex() {
3742
if (runningAgainstLeaderCluster == false) {
3843
final Request request = new Request("POST", "/follower/_ccr/create_and_follow");
3944
request.setJsonEntity("{\"leader_index\": \"leader_cluster:leader\"}");
40-
assertLicenseIncompatible(request);
45+
assertNonCompliantLicense(request);
4146
}
4247
}
4348

44-
private static void assertLicenseIncompatible(final Request request) {
49+
public void testAutoFollow() throws Exception {
50+
if (runningAgainstLeaderCluster == false) {
51+
final Request request = new Request("PUT", "/_ccr/_auto_follow/leader_cluster");
52+
request.setJsonEntity("{\"leader_index_patterns\":[\"*\"]}");
53+
client().performRequest(request);
54+
55+
// parse the logs and ensure that the auto-coordinator skipped coordination on the leader cluster
56+
assertBusy(() -> {
57+
final List<String> lines = Files.readAllLines(PathUtils.get(System.getProperty("log")));
58+
59+
final Iterator<String> it = lines.iterator();
60+
61+
boolean warn = false;
62+
while (it.hasNext()) {
63+
final String line = it.next();
64+
if (line.matches(".*\\[WARN\\s*\\]\\[o\\.e\\.x\\.c\\.a\\.AutoFollowCoordinator\\s*\\] \\[node-0\\] " +
65+
"failure occurred during auto-follower coordination")) {
66+
warn = true;
67+
break;
68+
}
69+
}
70+
assertTrue(warn);
71+
assertTrue(it.hasNext());
72+
final String lineAfterWarn = it.next();
73+
assertThat(
74+
lineAfterWarn,
75+
equalTo("org.elasticsearch.ElasticsearchStatusException: " +
76+
"can not fetch remote cluster state as the remote cluster [leader_cluster] is not licensed for [ccr]; " +
77+
"the license mode [BASIC] on cluster [leader_cluster] does not enable [ccr]"));
78+
});
79+
}
80+
}
81+
82+
private static void assertNonCompliantLicense(final Request request) {
4583
final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
4684
final String expected = String.format(
4785
Locale.ROOT,

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public Collection<Object> createComponents(
132132

133133
return Arrays.asList(
134134
ccrLicenseChecker,
135-
new AutoFollowCoordinator(settings, client, threadPool, clusterService)
135+
new AutoFollowCoordinator(settings, client, threadPool, clusterService, ccrLicenseChecker)
136136
);
137137
}
138138

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Objects;
2424
import java.util.function.BooleanSupplier;
2525
import java.util.function.Consumer;
26+
import java.util.function.Function;
2627

2728
/**
2829
* Encapsulates licensing checking for CCR.
@@ -58,23 +59,89 @@ public boolean isCcrAllowed() {
5859

5960
/**
6061
* Fetches the leader index metadata from the remote cluster. Before fetching the index metadata, the remote cluster is checked for
61-
* license compatibility with CCR. If the remote cluster is not licensed for CCR, the {@link ActionListener#onFailure(Exception)} method
62-
* of the specified listener is invoked. Otherwise, the specified consumer is invoked with the leader index metadata fetched from the
63-
* remote cluster.
62+
* license compatibility with CCR. If the remote cluster is not licensed for CCR, the {@code onFailure} consumer is is invoked.
63+
* Otherwise, the specified consumer is invoked with the leader index metadata fetched from the remote cluster.
6464
*
6565
* @param client the client
6666
* @param clusterAlias the remote cluster alias
6767
* @param leaderIndex the name of the leader index
68-
* @param listener the listener
68+
* @param onFailure the failure consumer
6969
* @param leaderIndexMetadataConsumer the leader index metadata consumer
7070
* @param <T> the type of response the listener is waiting for
7171
*/
7272
public <T> void checkRemoteClusterLicenseAndFetchLeaderIndexMetadata(
7373
final Client client,
7474
final String clusterAlias,
7575
final String leaderIndex,
76-
final ActionListener<T> listener,
76+
final Consumer<Exception> onFailure,
7777
final Consumer<IndexMetaData> leaderIndexMetadataConsumer) {
78+
79+
final ClusterStateRequest request = new ClusterStateRequest();
80+
request.clear();
81+
request.metaData(true);
82+
request.indices(leaderIndex);
83+
checkRemoteClusterLicenseAndFetchClusterState(
84+
client,
85+
clusterAlias,
86+
request,
87+
onFailure,
88+
leaderClusterState -> leaderIndexMetadataConsumer.accept(leaderClusterState.getMetaData().index(leaderIndex)),
89+
licenseCheck -> indexMetadataNonCompliantRemoteLicense(leaderIndex, licenseCheck),
90+
e -> indexMetadataUnknownRemoteLicense(leaderIndex, clusterAlias, e));
91+
}
92+
93+
/**
94+
* Fetches the leader cluster state from the remote cluster by the specified cluster state request. Before fetching the cluster state,
95+
* the remote cluster is checked for license compliance with CCR. If the remote cluster is not licensed for CCR,
96+
* the {@code onFailure} consumer is invoked. Otherwise, the specified consumer is invoked with the leader cluster state fetched from
97+
* the remote cluster.
98+
*
99+
* @param client the client
100+
* @param clusterAlias the remote cluster alias
101+
* @param request the cluster state request
102+
* @param onFailure the failure consumer
103+
* @param leaderClusterStateConsumer the leader cluster state consumer
104+
* @param <T> the type of response the listener is waiting for
105+
*/
106+
public <T> void checkRemoteClusterLicenseAndFetchClusterState(
107+
final Client client,
108+
final String clusterAlias,
109+
final ClusterStateRequest request,
110+
final Consumer<Exception> onFailure,
111+
final Consumer<ClusterState> leaderClusterStateConsumer) {
112+
checkRemoteClusterLicenseAndFetchClusterState(
113+
client,
114+
clusterAlias,
115+
request,
116+
onFailure,
117+
leaderClusterStateConsumer,
118+
CcrLicenseChecker::clusterStateNonCompliantRemoteLicense,
119+
e -> clusterStateUnknownRemoteLicense(clusterAlias, e));
120+
}
121+
122+
/**
123+
* Fetches the leader cluster state from the remote cluster by the specified cluster state request. Before fetching the cluster state,
124+
* the remote cluster is checked for license compliance with CCR. If the remote cluster is not licensed for CCR,
125+
* the {@code onFailure} consumer is invoked. Otherwise, the specified consumer is invoked with the leader cluster state fetched from
126+
* the remote cluster.
127+
*
128+
* @param client the client
129+
* @param clusterAlias the remote cluster alias
130+
* @param request the cluster state request
131+
* @param onFailure the failure consumer
132+
* @param leaderClusterStateConsumer the leader cluster state consumer
133+
* @param nonCompliantLicense the supplier for when the license state of the remote cluster is non-compliant
134+
* @param unknownLicense the supplier for when the license state of the remote cluster is unknown due to failure
135+
* @param <T> the type of response the listener is waiting for
136+
*/
137+
private <T> void checkRemoteClusterLicenseAndFetchClusterState(
138+
final Client client,
139+
final String clusterAlias,
140+
final ClusterStateRequest request,
141+
final Consumer<Exception> onFailure,
142+
final Consumer<ClusterState> leaderClusterStateConsumer,
143+
final Function<RemoteClusterLicenseChecker.LicenseCheck, ElasticsearchStatusException> nonCompliantLicense,
144+
final Function<Exception, ElasticsearchStatusException> unknownLicense) {
78145
// we have to check the license on the remote cluster
79146
new RemoteClusterLicenseChecker(client, XPackLicenseState::isCcrAllowedForOperationMode).checkRemoteClusterLicenses(
80147
Collections.singletonList(clusterAlias),
@@ -83,35 +150,25 @@ public <T> void checkRemoteClusterLicenseAndFetchLeaderIndexMetadata(
83150
@Override
84151
public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) {
85152
if (licenseCheck.isSuccess()) {
86-
final Client remoteClient = client.getRemoteClusterClient(clusterAlias);
87-
final ClusterStateRequest clusterStateRequest = new ClusterStateRequest();
88-
clusterStateRequest.clear();
89-
clusterStateRequest.metaData(true);
90-
clusterStateRequest.indices(leaderIndex);
91-
final ActionListener<ClusterStateResponse> clusterStateListener = ActionListener.wrap(
92-
r -> {
93-
final ClusterState remoteClusterState = r.getState();
94-
final IndexMetaData leaderIndexMetadata =
95-
remoteClusterState.getMetaData().index(leaderIndex);
96-
leaderIndexMetadataConsumer.accept(leaderIndexMetadata);
97-
},
98-
listener::onFailure);
153+
final Client leaderClient = client.getRemoteClusterClient(clusterAlias);
154+
final ActionListener<ClusterStateResponse> clusterStateListener =
155+
ActionListener.wrap(s -> leaderClusterStateConsumer.accept(s.getState()), onFailure);
99156
// following an index in remote cluster, so use remote client to fetch leader index metadata
100-
remoteClient.admin().cluster().state(clusterStateRequest, clusterStateListener);
157+
leaderClient.admin().cluster().state(request, clusterStateListener);
101158
} else {
102-
listener.onFailure(incompatibleRemoteLicense(leaderIndex, licenseCheck));
159+
onFailure.accept(nonCompliantLicense.apply(licenseCheck));
103160
}
104161
}
105162

106163
@Override
107164
public void onFailure(final Exception e) {
108-
listener.onFailure(unknownRemoteLicense(leaderIndex, clusterAlias, e));
165+
onFailure.accept(unknownLicense.apply(e));
109166
}
110167

111168
});
112169
}
113170

114-
private static ElasticsearchStatusException incompatibleRemoteLicense(
171+
private static ElasticsearchStatusException indexMetadataNonCompliantRemoteLicense(
115172
final String leaderIndex, final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) {
116173
final String clusterAlias = licenseCheck.remoteClusterLicenseInfo().clusterAlias();
117174
final String message = String.format(
@@ -127,7 +184,21 @@ private static ElasticsearchStatusException incompatibleRemoteLicense(
127184
return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST);
128185
}
129186

130-
private static ElasticsearchStatusException unknownRemoteLicense(
187+
private static ElasticsearchStatusException clusterStateNonCompliantRemoteLicense(
188+
final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) {
189+
final String clusterAlias = licenseCheck.remoteClusterLicenseInfo().clusterAlias();
190+
final String message = String.format(
191+
Locale.ROOT,
192+
"can not fetch remote cluster state as the remote cluster [%s] is not licensed for [ccr]; %s",
193+
clusterAlias,
194+
RemoteClusterLicenseChecker.buildErrorMessage(
195+
"ccr",
196+
licenseCheck.remoteClusterLicenseInfo(),
197+
RemoteClusterLicenseChecker::isLicensePlatinumOrTrial));
198+
return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST);
199+
}
200+
201+
private static ElasticsearchStatusException indexMetadataUnknownRemoteLicense(
131202
final String leaderIndex, final String clusterAlias, final Exception cause) {
132203
final String message = String.format(
133204
Locale.ROOT,
@@ -138,4 +209,11 @@ private static ElasticsearchStatusException unknownRemoteLicense(
138209
return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST, cause);
139210
}
140211

212+
private static ElasticsearchStatusException clusterStateUnknownRemoteLicense(final String clusterAlias, final Exception cause) {
213+
final String message = String.format(
214+
Locale.ROOT,
215+
"can not fetch remote cluster state as the license state of the remote cluster [%s] could not be determined", clusterAlias);
216+
return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST, cause);
217+
}
218+
141219
}

0 commit comments

Comments
 (0)