From a9f3619b5a10989ca8bed415a2dbb521b67c3739 Mon Sep 17 00:00:00 2001 From: Areek Zillur Date: Tue, 22 Nov 2016 11:42:51 -0500 Subject: [PATCH] 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@b5c003decd55be7458186ecd004b2207398a7665 --- .../org/elasticsearch/license/License.java | 25 ++- .../license/LicensesMetaData.java | 16 +- .../org/elasticsearch/license/Licensing.java | 12 +- qa/tribe-tests-with-license/build.gradle | 72 ++++++++ .../elasticsearch/test/LicensingTribeIT.java | 162 ++++++++++++++++++ 5 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 qa/tribe-tests-with-license/build.gradle create mode 100644 qa/tribe-tests-with-license/src/test/java/org/elasticsearch/test/LicensingTribeIT.java diff --git a/elasticsearch/src/main/java/org/elasticsearch/license/License.java b/elasticsearch/src/main/java/org/elasticsearch/license/License.java index 2b457b7587a..1419ac8db93 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/license/License.java +++ b/elasticsearch/src/main/java/org/elasticsearch/license/License.java @@ -72,14 +72,27 @@ public class License implements ToXContent { * Decouples operation mode of a license from the license type value. *

* 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 opMode1 is greater than opMode2 */ + 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)) { diff --git a/elasticsearch/src/main/java/org/elasticsearch/license/LicensesMetaData.java b/elasticsearch/src/main/java/org/elasticsearch/license/LicensesMetaData.java index 326f6eae907..41dcc07bb69 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/license/LicensesMetaData.java +++ b/elasticsearch/src/main/java/org/elasticsearch/license/LicensesMetaData.java @@ -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 implements MetaData.Custom { +class LicensesMetaData extends AbstractDiffable implements MetaData.Custom, + TribeService.MergableCustomMetaData { public static final String TYPE = "licenses"; @@ -138,6 +141,17 @@ class LicensesMetaData extends AbstractDiffable 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"; } diff --git a/elasticsearch/src/main/java/org/elasticsearch/license/Licensing.java b/elasticsearch/src/main/java/org/elasticsearch/license/Licensing.java index 06b52255ac7..ddd33e197c6 100644 --- a/elasticsearch/src/main/java/org/elasticsearch/license/Licensing.java +++ b/elasticsearch/src/main/java/org/elasticsearch/license/Licensing.java @@ -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> 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> getRestHandlers() { if (isTribeNode) { - return emptyList(); + return Collections.singletonList(RestGetLicenseAction.class); } return Arrays.asList(RestPutLicenseAction.class, RestGetLicenseAction.class, diff --git a/qa/tribe-tests-with-license/build.gradle b/qa/tribe-tests-with-license/build.gradle new file mode 100644 index 00000000000..0c6a81f7adf --- /dev/null +++ b/qa/tribe-tests-with-license/build.gradle @@ -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 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 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' +} \ No newline at end of file diff --git a/qa/tribe-tests-with-license/src/test/java/org/elasticsearch/test/LicensingTribeIT.java b/qa/tribe-tests-with-license/src/test/java/org/elasticsearch/test/LicensingTribeIT.java new file mode 100644 index 00000000000..76baa8b8f18 --- /dev/null +++ b/qa/tribe-tests-with-license/src/test/java/org/elasticsearch/test/LicensingTribeIT.java @@ -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> nodePlugins() { + return Collections.singletonList(XPackPlugin.class); + } + + @Override + protected Collection> 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"; +}