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 */
@@ -399,7 +405,7 @@ private static boolean isBasic(OperationMode mode) {
399405 return mode == OperationMode .BASIC ;
400406 }
401407
402- /** A wrapper for the license mode and state , to allow atomically swapping. */
408+ /** A wrapper for the license mode, state, and expiration date , to allow atomically swapping. */
403409 private static class Status {
404410
405411 /** The current "mode" of the license (ie license type). */
@@ -408,9 +414,13 @@ private static class Status {
408414 /** True if the license is active, or false if it is expired. */
409415 final boolean active ;
410416
411- Status (OperationMode mode , boolean active ) {
417+ /** The current expiration date of the license; Long.MAX_VALUE if not available yet. */
418+ final long licenseExpiryDate ;
419+
420+ Status (OperationMode mode , boolean active , long licenseExpiryDate ) {
412421 this .mode = mode ;
413422 this .active = active ;
423+ this .licenseExpiryDate = licenseExpiryDate ;
414424 }
415425 }
416426
@@ -424,7 +434,7 @@ private static class Status {
424434 // XPackLicenseState. However, if status is read multiple times in a method, it can change in between
425435 // reads. Methods should use `executeAgainstStatus` and `checkAgainstStatus` to ensure that the status
426436 // is only read once.
427- private volatile Status status = new Status (OperationMode .TRIAL , true );
437+ private volatile Status status = new Status (OperationMode .TRIAL , true , Long . MAX_VALUE );
428438
429439 public XPackLicenseState (Settings settings , LongSupplier epochMillisProvider ) {
430440 this .listeners = new CopyOnWriteArrayList <>();
@@ -472,12 +482,13 @@ private boolean checkAgainstStatus(Predicate<Status> statusPredicate) {
472482 *
473483 * @param mode The mode (type) of the current license.
474484 * @param active True if the current license exists and is within its allowed usage period; false if it is expired or missing.
485+ * @param expirationDate Expiration date of the current license.
475486 * @param mostRecentTrialVersion If this cluster has, at some point commenced a trial, the most recent version on which they did that.
476487 * May be {@code null} if they have never generated a trial license on this cluster, or the most recent
477488 * trial was prior to this metadata being tracked (6.1)
478489 */
479- void update (OperationMode mode , boolean active , @ Nullable Version mostRecentTrialVersion ) {
480- status = new Status (mode , active );
490+ void update (OperationMode mode , boolean active , long expirationDate , @ Nullable Version mostRecentTrialVersion ) {
491+ status = new Status (mode , active , expirationDate );
481492 listeners .forEach (LicenseStateListener ::licenseStateChanged );
482493 }
483494
@@ -513,12 +524,26 @@ boolean isActive() {
513524 /**
514525 * Checks whether the given feature is allowed, tracking the last usage time.
515526 */
527+ @ SuppressForbidden (reason = "Argument to Math.abs() is definitely not Long.MIN_VALUE" )
516528 public boolean checkFeature (Feature feature ) {
517529 boolean allowed = isAllowed (feature );
518530 LongAccumulator maxEpochAccumulator = lastUsed .get (feature );
531+ final long licenseExpiryDate = getLicenseExpiryDate ();
532+ final long diff = licenseExpiryDate - System .currentTimeMillis ();
519533 if (maxEpochAccumulator != null ) {
520534 maxEpochAccumulator .accumulate (epochMillisProvider .getAsLong ());
521535 }
536+
537+ if (feature .minimumOperationMode .compareTo (OperationMode .BASIC ) > 0 &&
538+ LICENSE_EXPIRATION_WARNING_PERIOD .getMillis () > diff ) {
539+ final long days = TimeUnit .MILLISECONDS .toDays (diff );
540+ final String expiryMessage = (days == 0 && diff > 0 )? "expires today" :
541+ (diff > 0 ? String .format (Locale .ROOT , "will expire in [%d] days" , days ):
542+ String .format (Locale .ROOT , "expired on [%s]" , LicenseService .DATE_FORMATTER .formatMillis (licenseExpiryDate )));
543+ HeaderWarning .addWarning ("Your license {}. " +
544+ "Contact your administrator or update your license for continued use of features" , expiryMessage );
545+ }
546+
522547 return allowed ;
523548 }
524549
@@ -635,6 +660,11 @@ public boolean isAllowedByLicense(OperationMode minimumMode, boolean needActive)
635660 });
636661 }
637662
663+ /** Return the current license expiration date. */
664+ public long getLicenseExpiryDate () {
665+ return executeAgainstStatus (status -> status .licenseExpiryDate );
666+ }
667+
638668 /**
639669 * A convenient method to test whether a feature is by license status.
640670 * @see #isAllowedByLicense(OperationMode, boolean)
0 commit comments