Skip to content

Commit

Permalink
Re-introduce LicenseService as an interface (#94646)
Browse files Browse the repository at this point in the history
This commit introduces an interface and contract for the license service .
The contract is split into a read only interface (parent) and mutable interface
(sub interface). Attempts are made to avoid leaking implementation
details in the interface. As a result there are a couple places
early in the lifecycle where we need to intanceof check and
downcast to avoid a leaky contract but those are special cases.

Usage of the interface should generally be constrained to
@Inject the LicenseService and call getXPackLicenseState()
as input to the licensed feature check, or to inject the service
into transport actions.

The implementation is now wired in via the new PluginComponentBinding
which allows plugin components, which are interfaces, to be @Inject to
the relevant actions.

This commit builds on
#94171
#94347
#94261
#94566
  • Loading branch information
jakelandis committed Mar 30, 2023
1 parent bec25f0 commit f4fd953
Show file tree
Hide file tree
Showing 33 changed files with 331 additions and 197 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.ClusterStateLicenseService;
import org.elasticsearch.license.LicenseService;
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
import org.elasticsearch.protocol.xpack.XPackUsageRequest;
Expand Down Expand Up @@ -62,10 +62,10 @@ public static class DataTiersTransportXPackInfoAction extends TransportXPackInfo
public DataTiersTransportXPackInfoAction(
TransportService transportService,
ActionFilters actionFilters,
ClusterStateLicenseService clusterStateLicenseService,
LicenseService licenseService,
NodeClient client
) {
super(transportService, actionFilters, clusterStateLicenseService, client);
super(transportService, actionFilters, licenseService, client);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.cluster.AckedClusterStateUpdateTask;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterState;
Expand Down Expand Up @@ -53,7 +54,11 @@
* It also listens on all nodes for cluster state updates, and updates {@link XPackLicenseState} when
* the license changes are detected in the cluster state.
*/
public class ClusterStateLicenseService extends AbstractLifecycleComponent implements ClusterStateListener, SchedulerEngine.Listener {
public class ClusterStateLicenseService extends AbstractLifecycleComponent
implements
LicenseService.MutableLicenseService,
ClusterStateListener,
SchedulerEngine.Listener {
private static final Logger logger = LogManager.getLogger(ClusterStateLicenseService.class);

private final Settings settings;
Expand All @@ -63,7 +68,7 @@ public class ClusterStateLicenseService extends AbstractLifecycleComponent imple
/**
* The xpack feature state to update when license changes are made.
*/
private final XPackLicenseState licenseState;
private final XPackLicenseState xPacklicenseState;

/**
* Currently active license
Expand Down Expand Up @@ -98,7 +103,7 @@ public ClusterStateLicenseService(
ThreadPool threadPool,
ClusterService clusterService,
Clock clock,
XPackLicenseState licenseState
XPackLicenseState xPacklicenseState
) {
this.settings = settings;
this.clusterService = clusterService;
Expand All @@ -114,12 +119,12 @@ public ClusterStateLicenseService(
);
this.clock = clock;
this.scheduler = new SchedulerEngine(settings, clock);
this.licenseState = licenseState;
this.xPacklicenseState = xPacklicenseState;
this.allowedLicenseTypes = LicenseSettings.ALLOWED_LICENSE_TYPES_SETTING.get(settings);
this.scheduler.register(this);
populateExpirationCallbacks();

threadPool.scheduleWithFixedDelay(licenseState::cleanupUsageTracking, TimeValue.timeValueHours(1), ThreadPool.Names.GENERIC);
threadPool.scheduleWithFixedDelay(xPacklicenseState::cleanupUsageTracking, TimeValue.timeValueHours(1), ThreadPool.Names.GENERIC);
}

private void logExpirationWarning(long expirationMillis, boolean expired) {
Expand Down Expand Up @@ -177,7 +182,8 @@ public void on(License license) {
* Registers new license in the cluster
* Master only operation. Installs a new license on the master provided it is VALID
*/
public void registerLicense(final PutLicenseRequest request, final ActionListener<PutLicenseResponse> listener) {
@Override
public void registerLicense(PutLicenseRequest request, ActionListener<PutLicenseResponse> listener) {
final License newLicense = request.license();
final long now = clock.millis();
if (LicenseVerifier.verifyLicense(newLicense) == false || newLicense.issueDate() > now || newLicense.startDate() > now) {
Expand Down Expand Up @@ -281,7 +287,7 @@ public void triggered(SchedulerEngine.Event event) {
if (licensesMetadata != null) {
final License license = licensesMetadata.getLicense();
if (event.getJobName().equals(LICENSE_JOB)) {
updateLicenseState(license);
updateXPackLicenseState(license);
} else if (event.getJobName().startsWith(ExpirationCallback.EXPIRATION_JOB_PREFIX)) {
expirationCallbacks.stream()
.filter(expirationCallback -> expirationCallback.getId().equals(event.getJobName()))
Expand All @@ -293,19 +299,22 @@ public void triggered(SchedulerEngine.Event event) {
/**
* Remove license from the cluster state metadata
*/
public void removeLicense(final ActionListener<PostStartBasicResponse> listener) {
@Override
public void removeLicense(ActionListener<? extends AcknowledgedResponse> listener) {
final PostStartBasicRequest startBasicRequest = new PostStartBasicRequest().acknowledge(true);
@SuppressWarnings("unchecked")
final StartBasicClusterTask task = new StartBasicClusterTask(
logger,
clusterService.getClusterName().value(),
clock,
startBasicRequest,
"delete license",
listener
(ActionListener<PostStartBasicResponse>) listener
);
startBasicTaskQueue.submitTask(task.getDescription(), task, null); // TODO should pass in request.masterNodeTimeout() here
}

@Override
public License getLicense() {
final License license = getLicense(clusterService.state().metadata());
return license == LicensesMetadata.LICENSE_TOMBSTONE ? null : license;
Expand All @@ -315,7 +324,8 @@ private LicensesMetadata getLicensesMetadata() {
return this.clusterService.state().metadata().custom(LicensesMetadata.TYPE);
}

void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostStartTrialResponse> listener) {
@Override
public void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostStartTrialResponse> listener) {
License.LicenseType requestedType = License.LicenseType.parse(request.getType());
if (LicenseSettings.VALID_TRIAL_TYPES.contains(requestedType) == false) {
throw new IllegalArgumentException(
Expand All @@ -336,7 +346,8 @@ void startTrialLicense(PostStartTrialRequest request, final ActionListener<PostS
);
}

void startBasicLicense(PostStartBasicRequest request, final ActionListener<PostStartBasicResponse> listener) {
@Override
public void startBasicLicense(PostStartBasicRequest request, final ActionListener<PostStartBasicResponse> listener) {
StartBasicClusterTask task = new StartBasicClusterTask(
logger,
clusterService.getClusterName().value(),
Expand Down Expand Up @@ -474,28 +485,36 @@ protected String getExpiryWarning(long licenseExpiryDate, long currentTime) {
return null;
}

protected void updateLicenseState(final License license) {
private void updateXPackLicenseState(License license) {
long time = clock.millis();
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
// implies license has been explicitly deleted
licenseState.update(License.OperationMode.MISSING, false, getExpiryWarning(LicenseUtils.getExpiryDate(license), time));
xPacklicenseState.update(License.OperationMode.MISSING, false, getExpiryWarning(LicenseUtils.getExpiryDate(license), time));
return;
}
checkForExpiredLicense(license);
}

private boolean checkForExpiredLicense(License license) {
long time = clock.millis();
if (license != null) {
final boolean active;
if (LicenseUtils.getExpiryDate(license) == LicenseSettings.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
active = true;
} else {
active = time >= license.issueDate() && time < LicenseUtils.getExpiryDate(license);
}
licenseState.update(license.operationMode(), active, getExpiryWarning(LicenseUtils.getExpiryDate(license), time));
xPacklicenseState.update(license.operationMode(), active, getExpiryWarning(LicenseUtils.getExpiryDate(license), time));

if (active) {
logger.debug("license [{}] - valid", license.uid());
return false;
} else {
logger.warn("license [{}] - expired", license.uid());
return true;
}
}
return false;
}

/**
Expand All @@ -505,7 +524,7 @@ protected void updateLicenseState(final License license) {
* relative to the current license's expiry
*/
private void onUpdate(final LicensesMetadata currentLicensesMetadata) {
final License license = getLicense(currentLicensesMetadata);
final License license = getLicenseFromLicensesMetadata(currentLicensesMetadata);
// license can be null if the trial license is yet to be auto-generated
// in this case, it is a no-op
if (license != null) {
Expand All @@ -527,7 +546,7 @@ private void onUpdate(final LicensesMetadata currentLicensesMetadata) {
}
logger.info("license [{}] mode [{}] - valid", license.uid(), license.operationMode().name().toLowerCase(Locale.ROOT));
}
updateLicenseState(license);
updateXPackLicenseState(license);
}
}

Expand All @@ -554,12 +573,13 @@ SchedulerEngine.Schedule nextLicenseCheck(License license) {
};
}

public static License getLicense(final Metadata metadata) {
public License getLicense(final Metadata metadata) {
final LicensesMetadata licensesMetadata = metadata.custom(LicensesMetadata.TYPE);
return getLicense(licensesMetadata);
return getLicenseFromLicensesMetadata(licensesMetadata);
}

static License getLicense(@Nullable final LicensesMetadata metadata) {
// visible for tests
License getLicenseFromLicensesMetadata(@Nullable final LicensesMetadata metadata) {
if (metadata != null) {
License license = metadata.getLicense();
if (license == LicensesMetadata.LICENSE_TOMBSTONE) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.license;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.common.component.LifecycleComponent;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;

/**
* Interface to read the current license. Consumers should generally not need to read the license directly and should instead
* prefer {@link XPackLicenseState} and/or {@link LicensedFeature} to make license decisions.
* <b>This interface is not intended to be implemented by alternative implementations and exists for internal use only.</b>
*/
public interface LicenseService extends LifecycleComponent {

/**
* Get the current license. Reading the license directly should generally be avoided and
* license decisions should generally prefer {@link XPackLicenseState} and/or {@link LicensedFeature}.
* @return the current license, null or {@link LicensesMetadata#LICENSE_TOMBSTONE} if no license is available.
*/
License getLicense();

/**
* Interface to update the current license.
* <b>This interface is not intended to be implemented by alternative implementations and exists for internal use only.</b>
*/
interface MutableLicenseService extends LicenseService, LifecycleComponent {

/**
* Creates or updates the current license as defined by the request.
*/
void registerLicense(PutLicenseRequest request, ActionListener<PutLicenseResponse> listener);

/**
* Removes the current license. Implementations should remove the current license and ensure that attempts to read returns
* {@link LicensesMetadata#LICENSE_TOMBSTONE} if a license was removed. Additionally the {@link XPackLicenseState} must be updated.
*/
void removeLicense(ActionListener<? extends AcknowledgedResponse> listener);

/**
* Installs a basic license.
*/
void startBasicLicense(PostStartBasicRequest request, ActionListener<PostStartBasicResponse> listener);

/**
* Installs a trial license.
*/
void startTrialLicense(PostStartTrialRequest request, ActionListener<PostStartTrialResponse> listener);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@

public class TransportDeleteLicenseAction extends AcknowledgedTransportMasterNodeAction<DeleteLicenseRequest> {

private final ClusterStateLicenseService clusterStateLicenseService;
private final LicenseService.MutableLicenseService licenseService;

@Inject
public TransportDeleteLicenseAction(
TransportService transportService,
ClusterService clusterService,
ClusterStateLicenseService clusterStateLicenseService,
LicenseService.MutableLicenseService licenseService,
ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver
Expand All @@ -45,7 +45,7 @@ public TransportDeleteLicenseAction(
indexNameExpressionResolver,
ThreadPool.Names.MANAGEMENT
);
this.clusterStateLicenseService = clusterStateLicenseService;
this.licenseService = licenseService;
}

@Override
Expand All @@ -60,7 +60,7 @@ protected void masterOperation(
ClusterState state,
final ActionListener<AcknowledgedResponse> listener
) throws ElasticsearchException {
clusterStateLicenseService.removeLicense(
licenseService.removeLicense(
listener.delegateFailure(
(l, postStartBasicResponse) -> l.onResponse(AcknowledgedResponse.of(postStartBasicResponse.isAcknowledged()))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@

public class TransportGetLicenseAction extends TransportMasterNodeReadAction<GetLicenseRequest, GetLicenseResponse> {

private final LicenseService licenseService;

@Inject
public TransportGetLicenseAction(
TransportService transportService,
ClusterService clusterService,
ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver
IndexNameExpressionResolver indexNameExpressionResolver,
LicenseService licenseService
) {
super(
GetLicenseAction.NAME,
Expand All @@ -42,6 +45,7 @@ public TransportGetLicenseAction(
GetLicenseResponse::new,
ThreadPool.Names.SAME
);
this.licenseService = licenseService;
}

@Override
Expand All @@ -56,6 +60,10 @@ protected void masterOperation(
ClusterState state,
final ActionListener<GetLicenseResponse> listener
) throws ElasticsearchException {
listener.onResponse(new GetLicenseResponse(ClusterStateLicenseService.getLicense(state.metadata())));
if (licenseService instanceof ClusterStateLicenseService clusterStateLicenseService) {
listener.onResponse(new GetLicenseResponse(clusterStateLicenseService.getLicense(state.metadata())));
} else {
listener.onResponse(new GetLicenseResponse(licenseService.getLicense()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@

public class TransportGetTrialStatusAction extends TransportMasterNodeReadAction<GetTrialStatusRequest, GetTrialStatusResponse> {

private final LicenseService licenseService;

@Inject
public TransportGetTrialStatusAction(
TransportService transportService,
ClusterService clusterService,
ThreadPool threadPool,
ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver
IndexNameExpressionResolver indexNameExpressionResolver,
LicenseService licenseService
) {
super(
GetTrialStatusAction.NAME,
Expand All @@ -40,6 +43,7 @@ public TransportGetTrialStatusAction(
GetTrialStatusResponse::new,
ThreadPool.Names.SAME
);
this.licenseService = licenseService;
}

@Override
Expand All @@ -49,9 +53,12 @@ protected void masterOperation(
ClusterState state,
ActionListener<GetTrialStatusResponse> listener
) throws Exception {
LicensesMetadata licensesMetadata = state.metadata().custom(LicensesMetadata.TYPE);
listener.onResponse(new GetTrialStatusResponse(licensesMetadata == null || licensesMetadata.isEligibleForTrial()));

if (licenseService instanceof ClusterStateLicenseService) {
LicensesMetadata licensesMetadata = state.metadata().custom(LicensesMetadata.TYPE);
listener.onResponse(new GetTrialStatusResponse(licensesMetadata == null || licensesMetadata.isEligibleForTrial()));
} else {
listener.onResponse(new GetTrialStatusResponse(false));
}
}

@Override
Expand Down

0 comments on commit f4fd953

Please sign in to comment.