Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions server/src/main/java/org/elasticsearch/common/util/Maps.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@

package org.elasticsearch.common.util;

import org.elasticsearch.Assertions;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class Maps {

Expand All @@ -43,4 +47,35 @@ public static <K, V> boolean deepEquals(Map<K, V> left, Map<K, V> right) {
.allMatch(e -> right.containsKey(e.getKey()) && Objects.deepEquals(e.getValue(), right.get(e.getKey())));
}

/**
* Remove the specified key from the provided immutable map by copying the underlying map and filtering out the specified
* key if that key exists.
*
* @param map the immutable map to remove the key from
* @param key the key to be removed
* @param <K> the type of the keys in the map
* @param <V> the type of the values in the map
* @return an immutable map that contains the items from the specified map with the provided key removed
*/
public static <K, V> Map<K, V> copyMapWithRemovedEntry(final Map<K, V> map, final K key) {
Objects.requireNonNull(map);
Objects.requireNonNull(key);
assertImmutableMap(map, key, map.get(key));
return map.entrySet().stream().filter(k -> key.equals(k.getKey()) == false)
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue),
Collections::<K, V>unmodifiableMap));
}

private static <K, V> void assertImmutableMap(final Map<K, V> map, final K key, final V value) {
if (Assertions.ENABLED) {
boolean immutable;
try {
map.put(key, value);
immutable = false;
} catch (final UnsupportedOperationException e) {
immutable = true;
}
assert immutable : "expected an immutable map but was [" + map.getClass() + "]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void sendResponse(RestResponse restResponse) {

// Add all custom headers
addCustomHeaders(httpResponse, restResponse.getHeaders());
addCustomHeaders(httpResponse, threadContext.getResponseHeaders());
addCustomHeaders(httpResponse, restResponse.filterHeaders(threadContext.getResponseHeaders()));

// If our response doesn't specify a content-type header, set one
setHeaderField(httpResponse, CONTENT_TYPE, restResponse.contentType(), false);
Expand Down
4 changes: 4 additions & 0 deletions server/src/main/java/org/elasticsearch/rest/RestResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,8 @@ public Map<String, List<String>> getHeaders() {
return customHeaders;
}
}

public Map<String, List<String>> filterHeaders(Map<String, List<String>> headers) {
return headers;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
*/
static final TimeValue GRACE_PERIOD_DURATION = days(7);

/**
* Period before the license expires when warning starts being added to the response header
*/
static final TimeValue LICENSE_EXPIRATION_WARNING_PERIOD = days(7);

public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS =
XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;

Expand Down Expand Up @@ -125,7 +130,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste

public static final String LICENSE_JOB = "licenseJob";

private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");
public static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMM dd, yyyy");

private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
Expand Down Expand Up @@ -476,7 +481,7 @@ private void updateLicenseState(LicensesMetadata licensesMetadata) {
protected void updateLicenseState(final License license, Version mostRecentTrialVersion) {
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
// implies license has been explicitly deleted
licenseState.update(License.OperationMode.MISSING, false, mostRecentTrialVersion);
licenseState.update(License.OperationMode.MISSING, false, license.expiryDate(), mostRecentTrialVersion);
return;
}
if (license != null) {
Expand All @@ -489,7 +494,7 @@ protected void updateLicenseState(final License license, Version mostRecentTrial
// date that is near Long.MAX_VALUE
active = time >= license.issueDate() && time - GRACE_PERIOD_DURATION.getMillis() < license.expiryDate();
}
licenseState.update(license.operationMode(), active, mostRecentTrialVersion);
licenseState.update(license.operationMode(), active, license.expiryDate(), mostRecentTrialVersion);

if (active) {
if (time < license.expiryDate()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public interface LicenseStateListener {

/**
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, Version)}.
* Callback when the license state changes. See {@link XPackLicenseState#update(License.OperationMode, boolean, long, Version)}.
*/
void licenseStateChanged();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.elasticsearch.Version;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.logging.HeaderWarning;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.License.OperationMode;
Expand All @@ -19,17 +21,21 @@
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.elasticsearch.license.LicenseService.LICENSE_EXPIRATION_WARNING_PERIOD;

/**
* A holder for the current state of the license for all xpack features.
*/
Expand Down Expand Up @@ -399,7 +405,7 @@ private static boolean isBasic(OperationMode mode) {
return mode == OperationMode.BASIC;
}

/** A wrapper for the license mode and state, to allow atomically swapping. */
/** A wrapper for the license mode, state, and expiration date, to allow atomically swapping. */
private static class Status {

/** The current "mode" of the license (ie license type). */
Expand All @@ -408,9 +414,13 @@ private static class Status {
/** True if the license is active, or false if it is expired. */
final boolean active;

Status(OperationMode mode, boolean active) {
/** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
final long licenseExpiryDate;

Status(OperationMode mode, boolean active, long licenseExpiryDate) {
this.mode = mode;
this.active = active;
this.licenseExpiryDate = licenseExpiryDate;
}
}

Expand All @@ -424,7 +434,7 @@ private static class Status {
// XPackLicenseState. However, if status is read multiple times in a method, it can change in between
// reads. Methods should use `executeAgainstStatus` and `checkAgainstStatus` to ensure that the status
// is only read once.
private volatile Status status = new Status(OperationMode.TRIAL, true);
private volatile Status status = new Status(OperationMode.TRIAL, true, Long.MAX_VALUE);

public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) {
this.listeners = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -472,12 +482,13 @@ private boolean checkAgainstStatus(Predicate<Status> statusPredicate) {
*
* @param mode The mode (type) of the current license.
* @param active True if the current license exists and is within its allowed usage period; false if it is expired or missing.
* @param expirationDate Expiration date of the current license.
* @param mostRecentTrialVersion If this cluster has, at some point commenced a trial, the most recent version on which they did that.
* May be {@code null} if they have never generated a trial license on this cluster, or the most recent
* trial was prior to this metadata being tracked (6.1)
*/
void update(OperationMode mode, boolean active, @Nullable Version mostRecentTrialVersion) {
status = new Status(mode, active);
void update(OperationMode mode, boolean active, long expirationDate, @Nullable Version mostRecentTrialVersion) {
status = new Status(mode, active, expirationDate);
listeners.forEach(LicenseStateListener::licenseStateChanged);
}

Expand Down Expand Up @@ -513,12 +524,26 @@ boolean isActive() {
/**
* Checks whether the given feature is allowed, tracking the last usage time.
*/
@SuppressForbidden(reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE")
public boolean checkFeature(Feature feature) {
boolean allowed = isAllowed(feature);
LongAccumulator maxEpochAccumulator = lastUsed.get(feature);
final long licenseExpiryDate = getLicenseExpiryDate();
final long diff = licenseExpiryDate - System.currentTimeMillis();
if (maxEpochAccumulator != null) {
maxEpochAccumulator.accumulate(epochMillisProvider.getAsLong());
}

if (feature.minimumOperationMode.compareTo(OperationMode.BASIC) > 0 &&
LICENSE_EXPIRATION_WARNING_PERIOD.getMillis() > diff) {
final long days = TimeUnit.MILLISECONDS.toDays(diff);
final String expiryMessage = (days == 0 && diff > 0)? "expires today":
(diff > 0? String.format(Locale.ROOT, "will expire in [%d] days", days):
String.format(Locale.ROOT, "expired on [%s]", LicenseService.DATE_FORMATTER.formatMillis(licenseExpiryDate)));
HeaderWarning.addWarning("Your license {}. " +
"Contact your administrator or update your license for continued use of features", expiryMessage);
}

return allowed;
}

Expand Down Expand Up @@ -635,6 +660,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
});
}

/** Return the current license expiration date. */
public long getLicenseExpiryDate() {
return executeAgainstStatus(status -> status.licenseExpiryDate);
}

/**
* A convenient method to test whether a feature is by license status.
* @see #isAllowedByLicense(OperationMode, boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,21 +360,23 @@ public static class AssertingLicenseState extends XPackLicenseState {
public final List<License.OperationMode> modeUpdates = new ArrayList<>();
public final List<Boolean> activeUpdates = new ArrayList<>();
public final List<Version> trialVersionUpdates = new ArrayList<>();
public final List<Long> expirationDateUpdates = new ArrayList<>();

public AssertingLicenseState() {
super(Settings.EMPTY, () -> 0);
}

@Override
void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
modeUpdates.add(mode);
activeUpdates.add(active);
expirationDateUpdates.add(expirationDate);
trialVersionUpdates.add(mostRecentTrialVersion);
}
}

/**
* A license state that makes the {@link #update(License.OperationMode, boolean, Version)}
* A license state that makes the {@link #update(License.OperationMode, boolean, long, Version)}
* method public for use in tests.
*/
public static class UpdatableLicenseState extends XPackLicenseState {
Expand All @@ -387,8 +389,8 @@ public UpdatableLicenseState(Settings settings) {
}

@Override
public void update(License.OperationMode mode, boolean active, Version mostRecentTrialVersion) {
super.update(mode, active, mostRecentTrialVersion);
public void update(License.OperationMode mode, boolean active, long expirationDate, Version mostRecentTrialVersion) {
super.update(mode, active, expirationDate, mostRecentTrialVersion);
}
}

Expand Down
Loading