Enable merging license in tribe node (elastic/elasticsearch#4147)
Currently, a tribe node ignored underlying cluster licenses due to inablity to select an appropriate license from multiple licenses. Now that tribe node supports merging custom metadata (elasticsearch#elastic/elasticsearch#21552), we can enable license support in tribe node. Now, tribe node chooses license with the highest operation mode from underlying cluster licenses. This commit also adds integration tests for licensing to verify that: - autogenerated trial license propagates to tribe node - tribe node chooses the highest operation mode license - removing a license from underlying cluster license is removed from tribe closes elastic/elasticsearch#3212 Original commit: elastic/x-pack-elasticsearch@b5c003decd
This commit is contained in:
parent
9eed90525d
commit
a9f3619b5a
|
@ -72,14 +72,27 @@ public class License implements ToXContent {
|
|||
* Decouples operation mode of a license from the license type value.
|
||||
* <p>
|
||||
* Note: The mode indicates features that should be made available, but it does not indicate whether the license is active!
|
||||
*
|
||||
* The id byte is used for ordering operation modes (used for merging license md in tribe node)
|
||||
*/
|
||||
public enum OperationMode {
|
||||
MISSING,
|
||||
TRIAL,
|
||||
BASIC,
|
||||
STANDARD,
|
||||
GOLD,
|
||||
PLATINUM;
|
||||
MISSING((byte) 0),
|
||||
TRIAL((byte) 1),
|
||||
BASIC((byte) 2),
|
||||
STANDARD((byte) 3),
|
||||
GOLD((byte) 4),
|
||||
PLATINUM((byte) 5);
|
||||
|
||||
private final byte id;
|
||||
|
||||
OperationMode(byte id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** Returns non-zero positive number when <code>opMode1</code> is greater than <code>opMode2</code> */
|
||||
public static int compare(OperationMode opMode1, OperationMode opMode2) {
|
||||
return Integer.compare(opMode1.id, opMode2.id);
|
||||
}
|
||||
|
||||
public static OperationMode resolve(String type) {
|
||||
switch (type.toLowerCase(Locale.ROOT)) {
|
||||
|
|
|
@ -11,6 +11,8 @@ import org.elasticsearch.common.io.stream.StreamInput;
|
|||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.License.OperationMode;
|
||||
import org.elasticsearch.tribe.TribeService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
@ -18,7 +20,8 @@ import java.util.EnumSet;
|
|||
/**
|
||||
* Contains metadata about registered licenses
|
||||
*/
|
||||
class LicensesMetaData extends AbstractDiffable<MetaData.Custom> implements MetaData.Custom {
|
||||
class LicensesMetaData extends AbstractDiffable<MetaData.Custom> implements MetaData.Custom,
|
||||
TribeService.MergableCustomMetaData<LicensesMetaData> {
|
||||
|
||||
public static final String TYPE = "licenses";
|
||||
|
||||
|
@ -138,6 +141,17 @@ class LicensesMetaData extends AbstractDiffable<MetaData.Custom> implements Meta
|
|||
return new LicensesMetaData(license);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LicensesMetaData merge(LicensesMetaData other) {
|
||||
if (other.license == null) {
|
||||
return this;
|
||||
} else if (license == null
|
||||
|| OperationMode.compare(other.license.operationMode(), license.operationMode()) > 0) {
|
||||
return other;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private static final class Fields {
|
||||
private static final String LICENSE = "license";
|
||||
}
|
||||
|
|
|
@ -29,7 +29,13 @@ public class Licensing implements ActionPlugin {
|
|||
private final boolean isTribeNode;
|
||||
|
||||
static {
|
||||
MetaData.registerPrototype(LicensesMetaData.TYPE, LicensesMetaData.PROTO);
|
||||
// we have to make sure we don't override the prototype, if we already
|
||||
// registered. This causes class cast exceptions while casting license
|
||||
// meta data on tribe node, as the registration happens for every tribe
|
||||
// client nodes and the tribe node itself
|
||||
if (MetaData.lookupPrototype(LicensesMetaData.TYPE) == null) {
|
||||
MetaData.registerPrototype(LicensesMetaData.TYPE, LicensesMetaData.PROTO);
|
||||
}
|
||||
}
|
||||
|
||||
public Licensing(Settings settings) {
|
||||
|
@ -41,7 +47,7 @@ public class Licensing implements ActionPlugin {
|
|||
@Override
|
||||
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
|
||||
if (isTribeNode) {
|
||||
return emptyList();
|
||||
return Collections.singletonList(new ActionHandler<>(GetLicenseAction.INSTANCE, TransportGetLicenseAction.class));
|
||||
}
|
||||
return Arrays.asList(new ActionHandler<>(PutLicenseAction.INSTANCE, TransportPutLicenseAction.class),
|
||||
new ActionHandler<>(GetLicenseAction.INSTANCE, TransportGetLicenseAction.class),
|
||||
|
@ -51,7 +57,7 @@ public class Licensing implements ActionPlugin {
|
|||
@Override
|
||||
public List<Class<? extends RestHandler>> getRestHandlers() {
|
||||
if (isTribeNode) {
|
||||
return emptyList();
|
||||
return Collections.singletonList(RestGetLicenseAction.class);
|
||||
}
|
||||
return Arrays.asList(RestPutLicenseAction.class,
|
||||
RestGetLicenseAction.class,
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import org.elasticsearch.gradle.test.ClusterConfiguration
|
||||
import org.elasticsearch.gradle.test.ClusterFormationTasks
|
||||
import org.elasticsearch.gradle.test.NodeInfo
|
||||
|
||||
apply plugin: 'elasticsearch.rest-test'
|
||||
|
||||
dependencies {
|
||||
testCompile project(path: ':x-plugins:elasticsearch', configuration: 'runtime')
|
||||
testCompile project(path: ':x-plugins:elasticsearch', configuration: 'testArtifacts')
|
||||
}
|
||||
|
||||
List<NodeInfo> cluster1Nodes
|
||||
|
||||
task setupClusterOne(type: DefaultTask) {
|
||||
mustRunAfter(precommit)
|
||||
ClusterConfiguration cluster1Config = new ClusterConfiguration(project)
|
||||
cluster1Config.clusterName = 'cluster1'
|
||||
cluster1Config.setting('node.name', 'cluster1-node1')
|
||||
// x-pack
|
||||
cluster1Config.plugin(':x-plugins:elasticsearch')
|
||||
cluster1Config.setting('xpack.monitoring.enabled', false)
|
||||
cluster1Config.setting('xpack.security.enabled', false)
|
||||
cluster1Config.setting('xpack.watcher.enabled', false)
|
||||
cluster1Config.setting('xpack.graph.enabled', false)
|
||||
|
||||
cluster1Nodes = ClusterFormationTasks.setup(project, setupClusterOne, cluster1Config)
|
||||
}
|
||||
|
||||
List<NodeInfo> cluster2Nodes
|
||||
|
||||
task setupClusterTwo(type: DefaultTask) {
|
||||
mustRunAfter(precommit)
|
||||
ClusterConfiguration cluster2Config = new ClusterConfiguration(project)
|
||||
cluster2Config.clusterName = 'cluster2'
|
||||
cluster2Config.setting('node.name', 'cluster2-node1')
|
||||
// x-pack
|
||||
cluster2Config.plugin(':x-plugins:elasticsearch')
|
||||
cluster2Config.setting('xpack.monitoring.enabled', false)
|
||||
cluster2Config.setting('xpack.monitoring.enabled', false)
|
||||
cluster2Config.setting('xpack.security.enabled', false)
|
||||
cluster2Config.setting('xpack.watcher.enabled', false)
|
||||
cluster2Config.setting('xpack.graph.enabled', false)
|
||||
|
||||
cluster2Nodes = ClusterFormationTasks.setup(project, setupClusterTwo, cluster2Config)
|
||||
}
|
||||
|
||||
integTest {
|
||||
dependsOn(setupClusterOne, setupClusterTwo)
|
||||
cluster {
|
||||
setting 'node.name', 'tribe-node'
|
||||
setting 'tribe.on_conflict', 'prefer_cluster1'
|
||||
setting 'tribe.cluster1.cluster.name', 'cluster1'
|
||||
setting 'tribe.cluster1.discovery.zen.ping.unicast.hosts', "'${-> cluster1Nodes.get(0).transportUri()}'"
|
||||
setting 'tribe.cluster1.http.enabled', 'true'
|
||||
setting 'tribe.cluster2.cluster.name', 'cluster2'
|
||||
setting 'tribe.cluster2.discovery.zen.ping.unicast.hosts', "'${-> cluster2Nodes.get(0).transportUri()}'"
|
||||
setting 'tribe.cluster2.http.enabled', 'true'
|
||||
// x-pack
|
||||
plugin ':x-plugins:elasticsearch'
|
||||
setting 'xpack.monitoring.enabled', false
|
||||
setting 'xpack.monitoring.enabled', false
|
||||
setting 'xpack.security.enabled', false
|
||||
setting 'xpack.watcher.enabled', false
|
||||
setting 'xpack.graph.enabled', false
|
||||
}
|
||||
systemProperty 'tests.cluster', "${-> cluster1Nodes.get(0).transportUri()}"
|
||||
systemProperty 'tests.cluster2', "${-> cluster2Nodes.get(0).transportUri()}"
|
||||
systemProperty 'tests.tribe', "${-> integTest.nodes.get(0).transportUri()}"
|
||||
// need to kill the standalone nodes here
|
||||
finalizedBy 'setupClusterOne#stop'
|
||||
finalizedBy 'setupClusterTwo#stop'
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.test;
|
||||
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.TransportAddress;
|
||||
import org.elasticsearch.license.GetLicenseResponse;
|
||||
import org.elasticsearch.license.License;
|
||||
import org.elasticsearch.license.LicensesStatus;
|
||||
import org.elasticsearch.license.LicensingClient;
|
||||
import org.elasticsearch.license.PutLicenseResponse;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.xpack.XPackPlugin;
|
||||
import org.elasticsearch.xpack.XPackSettings;
|
||||
import org.junit.AfterClass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
|
||||
public class LicensingTribeIT extends ESIntegTestCase {
|
||||
private static TestCluster cluster2;
|
||||
private static TestCluster tribeNode;
|
||||
|
||||
@Override
|
||||
protected Collection<Class<? extends Plugin>> nodePlugins() {
|
||||
return Collections.singletonList(XPackPlugin.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
|
||||
return nodePlugins();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (cluster2 == null) {
|
||||
cluster2 = buildExternalCluster(System.getProperty("tests.cluster2"));
|
||||
}
|
||||
if (tribeNode == null) {
|
||||
tribeNode = buildExternalCluster(System.getProperty("tests.tribe"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@AfterClass
|
||||
public static void tearDownExternalClusters() throws IOException {
|
||||
if (cluster2 != null) {
|
||||
try {
|
||||
cluster2.close();
|
||||
} finally {
|
||||
cluster2 = null;
|
||||
}
|
||||
}
|
||||
if (tribeNode != null) {
|
||||
try {
|
||||
tribeNode.close();
|
||||
} finally {
|
||||
tribeNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Settings externalClusterClientSettings() {
|
||||
Settings.Builder builder = Settings.builder();
|
||||
builder.put(XPackSettings.SECURITY_ENABLED.getKey(), false);
|
||||
builder.put(XPackSettings.MONITORING_ENABLED.getKey(), false);
|
||||
builder.put(XPackSettings.WATCHER_ENABLED.getKey(), false);
|
||||
builder.put(XPackSettings.GRAPH_ENABLED.getKey(), false);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ExternalTestCluster buildExternalCluster(String clusterAddresses) throws IOException {
|
||||
String[] stringAddresses = clusterAddresses.split(",");
|
||||
TransportAddress[] transportAddresses = new TransportAddress[stringAddresses.length];
|
||||
int i = 0;
|
||||
for (String stringAddress : stringAddresses) {
|
||||
URL url = new URL("http://" + stringAddress);
|
||||
InetAddress inetAddress = InetAddress.getByName(url.getHost());
|
||||
transportAddresses[i++] = new TransportAddress(new InetSocketAddress(inetAddress, url.getPort()));
|
||||
}
|
||||
return new ExternalTestCluster(createTempDir(), externalClusterClientSettings(), transportClientPlugins(), transportAddresses);
|
||||
}
|
||||
|
||||
public void testLicensePropagateToTribeNode() throws Exception {
|
||||
// test that auto-generated trial license propagates to tribe
|
||||
assertBusy(() -> {
|
||||
GetLicenseResponse getLicenseResponse = new LicensingClient(tribeNode.client()).prepareGetLicense().get();
|
||||
assertNotNull(getLicenseResponse.license());
|
||||
assertThat(getLicenseResponse.license().operationMode(), equalTo(License.OperationMode.TRIAL));
|
||||
});
|
||||
|
||||
// test that signed license put in one cluster propagates to tribe
|
||||
LicensingClient cluster1Client = new LicensingClient(client());
|
||||
PutLicenseResponse licenseResponse = cluster1Client.preparePutLicense(License.fromSource(BASIC_LICENSE))
|
||||
.setAcknowledge(true).get();
|
||||
assertThat(licenseResponse.isAcknowledged(), equalTo(true));
|
||||
assertThat(licenseResponse.status(), equalTo(LicensesStatus.VALID));
|
||||
assertBusy(() -> {
|
||||
GetLicenseResponse getLicenseResponse = new LicensingClient(tribeNode.client()).prepareGetLicense().get();
|
||||
assertNotNull(getLicenseResponse.license());
|
||||
assertThat(getLicenseResponse.license().operationMode(), equalTo(License.OperationMode.BASIC));
|
||||
});
|
||||
|
||||
// test that signed license with higher operation mode takes precedence
|
||||
LicensingClient cluster2Client = new LicensingClient(cluster2.client());
|
||||
licenseResponse = cluster2Client.preparePutLicense(License.fromSource(PLATINUM_LICENSE)).setAcknowledge(true).get();
|
||||
assertThat(licenseResponse.isAcknowledged(), equalTo(true));
|
||||
assertThat(licenseResponse.status(), equalTo(LicensesStatus.VALID));
|
||||
assertBusy(() -> {
|
||||
GetLicenseResponse getLicenseResponse = new LicensingClient(tribeNode.client()).prepareGetLicense().get();
|
||||
assertNotNull(getLicenseResponse.license());
|
||||
assertThat(getLicenseResponse.license().operationMode(), equalTo(License.OperationMode.PLATINUM));
|
||||
});
|
||||
|
||||
// test removing signed license falls back works
|
||||
assertTrue(cluster2Client.prepareDeleteLicense().get().isAcknowledged());
|
||||
assertBusy(() -> {
|
||||
GetLicenseResponse getLicenseResponse = new LicensingClient(tribeNode.client()).prepareGetLicense().get();
|
||||
assertNotNull(getLicenseResponse.license());
|
||||
assertThat(getLicenseResponse.license().operationMode(), equalTo(License.OperationMode.BASIC));
|
||||
});
|
||||
}
|
||||
|
||||
private static final String PLATINUM_LICENSE = "{\"license\":{\"uid\":\"1\",\"type\":\"platinum\"," +
|
||||
"\"issue_date_in_millis\":1411948800000,\"expiry_date_in_millis\":1914278399999,\"max_nodes\":1," +
|
||||
"\"issued_to\":\"issuedTo\",\"issuer\":\"issuer\"," +
|
||||
"\"signature\":\"AAAAAwAAAA2hWlkvKcxQIpdVWdCtAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1" +
|
||||
"PSmkxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0l" +
|
||||
"DalZCS095MXRGN1lIZlpYcVVTTnFrcTE2dzhJZmZrdFQrN3JQeGwxb0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVB" +
|
||||
"xRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1JOUml4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG5" +
|
||||
"3MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lxOHN5bUErZlNIWkZSVmZIWEtaSU9wTTJENDVvT1NCYkla" +
|
||||
"cUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZzlvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQBWg" +
|
||||
"u3yZp0KOBG//92X4YVmau3P5asvx0FAPDX2Ze734Tap/nc30X6Rt4yEEm+6bCQr/ibBOqWboJKRbbTZLBQfYFmL1ZqvAY3bJJ1/Xs" +
|
||||
"8NyDfxKGztlUt/IIOzHPzxs0f8Bv4OJeK48vjovWaDc1Vmo4n1SGyyL0JcEbOWC6A3U3mBsWn7wLUe+hW9+akVAYOO5TIcm60ub7k" +
|
||||
"H/LIZNOhvGglSVDbl3p8EBkNMy0CV7urQ0wdG1nLCnvf8/BiT15lC5nLrM9Dt5w3pzciPlASzw4iksW/CzvYy5tjOoWKEnxi2EZOB" +
|
||||
"9dKyT4mTdvyBOrTHLdgr4lmHd3qYAEgcTCaQ\",\"start_date_in_millis\":-1}}";
|
||||
|
||||
private static final String BASIC_LICENSE = "{\"license\":{\"uid\":\"1\",\"type\":\"basic\"," +
|
||||
"\"issue_date_in_millis\":1411948800000,\"expiry_date_in_millis\":1914278399999,\"max_nodes\":1," +
|
||||
"\"issued_to\":\"issuedTo\",\"issuer\":\"issuer\",\"signature\":\"AAA" + "AAwAAAA2is2oANL3mZGS883l9AAAB" +
|
||||
"mC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSmkxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3" +
|
||||
"lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0lDalZCS095MXRGN1lIZlpYcVVTTnFrcTE2dzhJZmZrdFQrN3JQeGwx" +
|
||||
"b0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVBxRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1JOUm" +
|
||||
"l4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG53MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lx" +
|
||||
"OHN5bUErZlNIWkZSVmZIWEtaSU9wTTJENDVvT1NCYklacUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZz" +
|
||||
"lvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQCjL9HJnHrHVRq39yO5OFrOS0fY+mf+KqLh8i+RK4s9Hepdi/VQ3SHTEonEUCCB" +
|
||||
"1iFO35eykW3t+poCMji9VGkslQyJ+uWKzUqn0lmioy8ukpjETcmKH8TSWTqcC7HNZ0NKc1XMTxwkIi/chQTsPUz+h3gfCHZRQwGnRz" +
|
||||
"JPmPjCJf4293hsMFUlsFQU3tYKDH+kULMdNx1Cg+3PhbUCNrUyQJMb5p4XDrwOaanZUM6HdifS1Y/qjxLXC/B1wHGFEpvrEPFyBuSe" +
|
||||
"GnJ9uxkrBSv28iG0qsyHrFhHQXIMVFlQKCPaMKikfuZyRhxzE5ntTcGJMn84llCaIyX/kmzqoZHQ\",\"start_date_in_millis\":-1}}\n";
|
||||
}
|
Loading…
Reference in New Issue