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:
Areek Zillur 2016-11-22 11:42:51 -05:00 committed by GitHub
parent 9eed90525d
commit a9f3619b5a
5 changed files with 277 additions and 10 deletions

View File

@ -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)) {

View File

@ -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";
}

View File

@ -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,

View File

@ -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'
}

View File

@ -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";
}