diff --git a/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/License.java b/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/License.java
index 403b9870f3c..f07d38edeed 100644
--- a/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/License.java
+++ b/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/License.java
@@ -101,6 +101,7 @@ public class License implements ToXContent {
case "gold":
return GOLD;
case "platinum":
+ case "cloud_internal":
case "internal": // bwc for 1.x subscription_type field
return PLATINUM;
default:
@@ -196,12 +197,42 @@ public class License implements ToXContent {
}
/**
- * @return the operation mode of the license as computed from the license type
+ * @return the operation mode of the license as computed from the license type or from
+ * the license mode file
*/
public OperationMode operationMode() {
+ synchronized (this) {
+ if (canReadOperationModeFromFile() && operationModeFileWatcher != null) {
+ return operationModeFileWatcher.getCurrentOperationMode();
+ }
+ }
return operationMode;
}
+ private boolean canReadOperationModeFromFile() {
+ return type.equals("cloud_internal");
+ }
+
+ private volatile OperationModeFileWatcher operationModeFileWatcher;
+
+ /**
+ * Sets the operation mode file watcher for the license and initializes the
+ * file watcher when the license type allows to override operation mode from file
+ */
+ public synchronized void setOperationModeFileWatcher(final OperationModeFileWatcher operationModeFileWatcher) {
+ this.operationModeFileWatcher = operationModeFileWatcher;
+ if (canReadOperationModeFromFile()) {
+ this.operationModeFileWatcher.init();
+ }
+ }
+
+ /**
+ * Removes operation mode file watcher, so unused license objects can be gc'ed
+ */
+ public synchronized void removeOperationModeFileWatcher() {
+ this.operationModeFileWatcher = null;
+ }
+
/**
* @return the current license's status
*/
diff --git a/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/OperationModeFileWatcher.java b/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/OperationModeFileWatcher.java
new file mode 100644
index 00000000000..3ba63e3cdd3
--- /dev/null
+++ b/elasticsearch/license/base/src/main/java/org/elasticsearch/license/core/OperationModeFileWatcher.java
@@ -0,0 +1,113 @@
+/*
+ * 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.license.core;
+
+
+import org.elasticsearch.common.logging.ESLogger;
+import org.elasticsearch.license.core.License.OperationMode;
+import org.elasticsearch.watcher.FileChangesListener;
+import org.elasticsearch.watcher.FileWatcher;
+import org.elasticsearch.watcher.ResourceWatcherService;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * File based watcher for license {@link OperationMode}
+ * Watches for changes in licenseModePath
, use
+ * {@link #getCurrentOperationMode()} to access the latest mode
+ *
+ * In case of failure to read a valid operation mode from licenseModePath
,
+ * the operation mode will default to PLATINUM
+ */
+public final class OperationModeFileWatcher extends FileChangesListener {
+ private final ResourceWatcherService resourceWatcherService;
+ private final Path licenseModePath;
+ private final AtomicBoolean initialized = new AtomicBoolean();
+ private final OperationMode defaultOperationMode = OperationMode.PLATINUM;
+ private volatile OperationMode currentOperationMode = defaultOperationMode;
+ private final ESLogger logger;
+ private final Runnable onChange;
+
+ public OperationModeFileWatcher(ResourceWatcherService resourceWatcherService, Path licenseModePath,
+ ESLogger logger, Runnable onChange) {
+ this.resourceWatcherService = resourceWatcherService;
+ this.licenseModePath = licenseModePath;
+ this.logger = logger;
+ this.onChange = onChange;
+ }
+
+ public void init() {
+ if (initialized.compareAndSet(false, true)) {
+ final FileWatcher watcher = new FileWatcher(licenseModePath);
+ watcher.addListener(this);
+ try {
+ resourceWatcherService.add(watcher, ResourceWatcherService.Frequency.HIGH);
+ if (Files.exists(licenseModePath)) {
+ onChange(licenseModePath);
+ }
+ } catch (IOException e) {
+ logger.error("couldn't initialize watching license mode file", e);
+ }
+ }
+ }
+
+ /**
+ * Returns the current operation mode based on license mode file.
+ * Defaults to {@link OperationMode#PLATINUM}
+ */
+ public OperationMode getCurrentOperationMode() {
+ return currentOperationMode;
+ }
+
+ @Override
+ public void onFileInit(Path file) {
+ onChange(file);
+ }
+
+ @Override
+ public void onFileCreated(Path file) {
+ onChange(file);
+ }
+
+ @Override
+ public void onFileDeleted(Path file) {
+ onChange(file);
+ }
+
+ @Override
+ public void onFileChanged(Path file) {
+ onChange(file);
+ }
+
+ private synchronized void onChange(Path file) {
+ if (file.equals(licenseModePath)) {
+ currentOperationMode = defaultOperationMode;
+ if (Files.exists(licenseModePath)
+ && Files.isReadable(licenseModePath)) {
+ final byte[] content;
+ try {
+ content = Files.readAllBytes(licenseModePath);
+ } catch (IOException e) {
+ logger.error("couldn't read operation mode from [{}]", e, licenseModePath.toAbsolutePath().toString());
+ return;
+ }
+ String operationMode = new String(content, StandardCharsets.UTF_8);
+ try {
+ currentOperationMode = OperationMode.resolve(operationMode);
+ } catch (IllegalArgumentException e) {
+ logger.error("invalid operation mode in [{}]", e, licenseModePath.toAbsolutePath().toString());
+ return;
+ }
+ }
+ onChange.run();
+ }
+ }
+}
+
diff --git a/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/LicenseOperationModeUpdateTests.java b/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/LicenseOperationModeUpdateTests.java
new file mode 100644
index 00000000000..8bd379862cc
--- /dev/null
+++ b/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/LicenseOperationModeUpdateTests.java
@@ -0,0 +1,73 @@
+/*
+ * 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.license.core;
+
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.watcher.FileWatcher;
+import org.elasticsearch.watcher.ResourceWatcherService;
+import org.junit.Before;
+
+import java.nio.file.Path;
+
+import static org.elasticsearch.license.core.OperationModeFileWatcherTests.writeMode;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class LicenseOperationModeUpdateTests extends ESTestCase {
+
+ private OperationModeFileWatcher operationModeFileWatcher;
+ private Path licenseModeFile;
+ private ResourceWatcherService resourceWatcherService;
+
+ @Before
+ public void init() throws Exception {
+ licenseModeFile = createTempFile();
+ resourceWatcherService = mock(ResourceWatcherService.class);
+ operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, licenseModeFile, logger, () -> {});
+ }
+
+ public void testLicenseOperationModeUpdate() throws Exception {
+ String type = randomFrom("trial", "basic", "standard", "gold", "platinum");
+ License license = License.builder()
+ .uid("id")
+ .expiryDate(0)
+ .issueDate(0)
+ .issuedTo("elasticsearch")
+ .issuer("issuer")
+ .type(type)
+ .maxNodes(1)
+ .build();
+
+ assertThat(license.operationMode(), equalTo(License.OperationMode.resolve(type)));
+ writeMode("gold", licenseModeFile);
+ license.setOperationModeFileWatcher(operationModeFileWatcher);
+ verifyZeroInteractions(resourceWatcherService);
+ assertThat(license.operationMode(), equalTo(License.OperationMode.resolve(type)));
+ }
+
+ public void testCloudInternalLicenseOperationModeUpdate() throws Exception {
+ License license = License.builder()
+ .uid("id")
+ .expiryDate(0)
+ .issueDate(0)
+ .issuedTo("elasticsearch")
+ .issuer("issuer")
+ .type("cloud_internal")
+ .maxNodes(1)
+ .build();
+
+ assertThat(license.operationMode(), equalTo(License.OperationMode.PLATINUM));
+ writeMode("gold", licenseModeFile);
+ license.setOperationModeFileWatcher(operationModeFileWatcher);
+ verify(resourceWatcherService, times(1)).add(any(FileWatcher.class), eq(ResourceWatcherService.Frequency.HIGH));
+ assertThat(license.operationMode(), equalTo(License.OperationMode.GOLD));
+ }
+}
diff --git a/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/OperationModeFileWatcherTests.java b/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/OperationModeFileWatcherTests.java
new file mode 100644
index 00000000000..a51ea032c91
--- /dev/null
+++ b/elasticsearch/license/base/src/test/java/org/elasticsearch/license/core/OperationModeFileWatcherTests.java
@@ -0,0 +1,114 @@
+/*
+ * 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.license.core;
+
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.threadpool.TestThreadPool;
+import org.elasticsearch.watcher.ResourceWatcherService;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class OperationModeFileWatcherTests extends ESTestCase {
+ private ResourceWatcherService watcherService;
+ private TestThreadPool threadPool;
+ private Path licenseModePath;
+ private OperationModeFileWatcher operationModeFileWatcher;
+ private AtomicInteger onChangeCounter;
+
+ @Before
+ public void setup() throws Exception {
+ threadPool = new TestThreadPool("license mode file watcher tests");
+ Settings settings = Settings.builder()
+ .put("resource.reload.interval.high", "10ms")
+ .build();
+ watcherService = new ResourceWatcherService(settings,
+ threadPool);
+ watcherService.start();
+ licenseModePath = createTempFile();
+ onChangeCounter = new AtomicInteger();
+ operationModeFileWatcher = new OperationModeFileWatcher(watcherService, licenseModePath, logger,
+ () -> onChangeCounter.incrementAndGet());
+ }
+
+ @After
+ public void shutdown() throws InterruptedException {
+ terminate(threadPool);
+ watcherService.stop();
+ }
+
+ public void testInit() throws Exception {
+ writeMode("gold");
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ operationModeFileWatcher.init();
+ assertThat(onChangeCounter.get(), equalTo(2));
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.GOLD));
+ }
+
+ public void testUpdateModeFromFile() throws Exception {
+ Files.delete(licenseModePath);
+ operationModeFileWatcher.init();
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ writeMode("gold");
+ assertBusy(() -> assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.GOLD)));
+ writeMode("basic");
+ assertBusy(() -> assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.BASIC)));
+ assertThat(onChangeCounter.get(), equalTo(2));
+ }
+
+ public void testDeleteModeFromFile() throws Exception {
+ Files.delete(licenseModePath);
+ operationModeFileWatcher.init();
+ writeMode("gold");
+ assertBusy(() -> assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.GOLD)));
+ Files.delete(licenseModePath);
+ assertBusy(() -> assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM)));
+ assertThat(onChangeCounter.get(), equalTo(2));
+ }
+
+ public void testInvalidModeFromFile() throws Exception {
+ writeMode("invalid");
+ operationModeFileWatcher.init();
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ operationModeFileWatcher.onFileChanged(licenseModePath);
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ }
+
+ public void testLicenseModeFileIsDirectory() throws Exception {
+ licenseModePath = createTempDir();
+ operationModeFileWatcher.init();
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ operationModeFileWatcher.onFileChanged(licenseModePath);
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ }
+
+ public void testLicenseModeFileCreatedAfterInit() throws Exception {
+ Files.delete(licenseModePath);
+ operationModeFileWatcher.init();
+ assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.PLATINUM));
+ Path tempFile = createTempFile();
+ writeMode("gold", tempFile);
+ licenseModePath = tempFile;
+ assertBusy(() -> assertThat(operationModeFileWatcher.getCurrentOperationMode(), equalTo(License.OperationMode.GOLD)));
+ }
+
+ private void writeMode(String mode) throws IOException {
+ writeMode(mode, licenseModePath);
+ }
+
+ static void writeMode(String mode, Path file) throws IOException {
+ Files.write(file, mode.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
+ }
+}
diff --git a/elasticsearch/x-pack/graph/src/main/java/org/elasticsearch/xpack/graph/GraphLicensee.java b/elasticsearch/x-pack/graph/src/main/java/org/elasticsearch/xpack/graph/GraphLicensee.java
index 90518bc8cee..7d31411597b 100644
--- a/elasticsearch/x-pack/graph/src/main/java/org/elasticsearch/xpack/graph/GraphLicensee.java
+++ b/elasticsearch/x-pack/graph/src/main/java/org/elasticsearch/xpack/graph/GraphLicensee.java
@@ -7,7 +7,6 @@ package org.elasticsearch.xpack.graph;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.license.core.License;
import org.elasticsearch.license.core.License.OperationMode;
import org.elasticsearch.license.plugin.core.AbstractLicenseeComponent;
import org.elasticsearch.license.plugin.core.LicenseState;
@@ -28,17 +27,15 @@ public class GraphLicensee extends AbstractLicenseeComponent {
}
@Override
- public String[] acknowledgmentMessages(License currentLicense, License newLicense) {
- switch (newLicense.operationMode()) {
+ public String[] acknowledgmentMessages(OperationMode currentMode, OperationMode newMode) {
+ switch (newMode) {
case BASIC:
case STANDARD:
case GOLD:
- if (currentLicense != null) {
- switch (currentLicense.operationMode()) {
- case TRIAL:
- case PLATINUM:
- return new String[] { "Graph will be disabled" };
- }
+ switch (currentMode) {
+ case TRIAL:
+ case PLATINUM:
+ return new String[] { "Graph will be disabled" };
}
break;
}
diff --git a/elasticsearch/x-pack/license-plugin/src/main/java/org/elasticsearch/license/plugin/Licensing.java b/elasticsearch/x-pack/license-plugin/src/main/java/org/elasticsearch/license/plugin/Licensing.java
index 4fcb7ee2fb1..7629a129b26 100644
--- a/elasticsearch/x-pack/license-plugin/src/main/java/org/elasticsearch/license/plugin/Licensing.java
+++ b/elasticsearch/x-pack/license-plugin/src/main/java/org/elasticsearch/license/plugin/Licensing.java
@@ -12,6 +12,7 @@ import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.env.Environment;
import org.elasticsearch.license.plugin.action.delete.DeleteLicenseAction;
import org.elasticsearch.license.plugin.action.delete.TransportDeleteLicenseAction;
import org.elasticsearch.license.plugin.action.get.GetLicenseAction;
@@ -25,6 +26,7 @@ import org.elasticsearch.license.plugin.rest.RestGetLicenseAction;
import org.elasticsearch.license.plugin.rest.RestPutLicenseAction;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.rest.RestHandler;
+import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.graph.GraphLicensee;
import org.elasticsearch.xpack.monitoring.MonitoringLicensee;
import org.elasticsearch.xpack.security.SecurityLicenseState;
@@ -83,14 +85,16 @@ public class Licensing implements ActionPlugin {
RestDeleteLicenseAction.class);
}
- public Collection