88import org .elasticsearch .Version ;
99import org .elasticsearch .common .Nullable ;
1010import org .elasticsearch .common .Strings ;
11+ import org .elasticsearch .common .SuppressForbidden ;
12+ import org .elasticsearch .common .logging .HeaderWarning ;
1113import org .elasticsearch .common .logging .LoggerMessageFormat ;
1214import org .elasticsearch .common .settings .Settings ;
1315import org .elasticsearch .license .License .OperationMode ;
1921import java .util .EnumMap ;
2022import java .util .LinkedHashMap ;
2123import java .util .List ;
24+ import java .util .Locale ;
2225import java .util .Map ;
2326import java .util .Objects ;
2427import java .util .Set ;
2528import java .util .concurrent .CopyOnWriteArrayList ;
29+ import java .util .concurrent .TimeUnit ;
2630import java .util .concurrent .atomic .LongAccumulator ;
2731import java .util .function .BiFunction ;
2832import java .util .function .Function ;
2933import java .util .function .LongSupplier ;
3034import java .util .function .Predicate ;
3135import java .util .stream .Collectors ;
3236
37+ import static org .elasticsearch .license .LicenseService .LICENSE_EXPIRATION_WARNING_PERIOD ;
38+
3339/**
3440 * A holder for the current state of the license for all xpack features.
3541 */
@@ -395,7 +401,7 @@ private static boolean isBasic(OperationMode mode) {
395401 return mode == OperationMode .BASIC ;
396402 }
397403
398- /** A wrapper for the license mode and state , to allow atomically swapping. */
404+ /** A wrapper for the license mode, state, and expiration date , to allow atomically swapping. */
399405 private static class Status {
400406
401407 /** The current "mode" of the license (ie license type). */
@@ -404,9 +410,13 @@ private static class Status {
404410 /** True if the license is active, or false if it is expired. */
405411 final boolean active ;
406412
407- Status (OperationMode mode , boolean active ) {
413+ /** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
414+ final long licenseExpiryDate ;
415+
416+ Status (OperationMode mode , boolean active , long licenseExpiryDate ) {
408417 this .mode = mode ;
409418 this .active = active ;
419+ this .licenseExpiryDate = licenseExpiryDate ;
410420 }
411421 }
412422
@@ -420,7 +430,7 @@ private static class Status {
420430 // XPackLicenseState. However, if status is read multiple times in a method, it can change in between
421431 // reads. Methods should use `executeAgainstStatus` and `checkAgainstStatus` to ensure that the status
422432 // is only read once.
423- private volatile Status status = new Status (OperationMode .TRIAL , true );
433+ private volatile Status status = new Status (OperationMode .TRIAL , true , Long . MAX_VALUE );
424434
425435 public XPackLicenseState (Settings settings , LongSupplier epochMillisProvider ) {
426436 this .listeners = new CopyOnWriteArrayList <>();
@@ -468,12 +478,13 @@ private boolean checkAgainstStatus(Predicate<Status> statusPredicate) {
468478 *
469479 * @param mode The mode (type) of the current license.
470480 * @param active True if the current license exists and is within its allowed usage period; false if it is expired or missing.
481+ * @param expirationDate Expiration date of the current license.
471482 * @param mostRecentTrialVersion If this cluster has, at some point commenced a trial, the most recent version on which they did that.
472483 * May be {@code null} if they have never generated a trial license on this cluster, or the most recent
473484 * trial was prior to this metadata being tracked (6.1)
474485 */
475- void update (OperationMode mode , boolean active , @ Nullable Version mostRecentTrialVersion ) {
476- status = new Status (mode , active );
486+ void update (OperationMode mode , boolean active , long expirationDate , @ Nullable Version mostRecentTrialVersion ) {
487+ status = new Status (mode , active , expirationDate );
477488 listeners .forEach (LicenseStateListener ::licenseStateChanged );
478489 }
479490
@@ -509,12 +520,26 @@ public boolean isActive() {
509520 /**
510521 * Checks whether the given feature is allowed, tracking the last usage time.
511522 */
523+ @ SuppressForbidden (reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE" )
512524 public boolean checkFeature (Feature feature ) {
513525 boolean allowed = isAllowed (feature );
514526 LongAccumulator maxEpochAccumulator = lastUsed .get (feature );
527+ final long licenseExpiryDate = getLicenseExpiryDate ();
528+ final long diff = licenseExpiryDate - System .currentTimeMillis ();
515529 if (maxEpochAccumulator != null ) {
516530 maxEpochAccumulator .accumulate (epochMillisProvider .getAsLong ());
517531 }
532+
533+ if (feature .minimumOperationMode .compareTo (OperationMode .BASIC ) > 0 &&
534+ LICENSE_EXPIRATION_WARNING_PERIOD .getMillis () > diff ) {
535+ final long days = TimeUnit .MILLISECONDS .toDays (diff );
536+ final String expiryMessage = (days == 0 && diff > 0 )? "expires today" :
537+ (diff > 0 ? String .format (Locale .ROOT , "will expire in [%d] days" , days ):
538+ String .format (Locale .ROOT , "expired on [%s]" , LicenseService .DATE_FORMATTER .formatMillis (licenseExpiryDate )));
539+ HeaderWarning .addWarning ("Your license {}. " +
540+ "Contact your administrator or update your license for continued use of features" , expiryMessage );
541+ }
542+
518543 return allowed ;
519544 }
520545
@@ -631,6 +656,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
631656 });
632657 }
633658
659+ /** Return the current license expiration date. */
660+ public long getLicenseExpiryDate () {
661+ return executeAgainstStatus (status -> status .licenseExpiryDate );
662+ }
663+
634664 /**
635665 * A convenient method to test whether a feature is by license status.
636666 * @see #isAllowedByLicense(OperationMode, boolean)
0 commit comments