com.sap.cds.services.mt.impl.MtDeploymentServiceHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-feature-mt Show documentation
Show all versions of cds-feature-mt Show documentation
Multi tenancy feature for CDS Services Java
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.services.mt.impl;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.feature.mt.MtUtils;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.EventContext;
import com.sap.cds.services.authentication.AuthenticationInfo;
import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo;
import com.sap.cds.services.environment.CdsProperties.MultiTenancy;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.mt.DeploymentService;
import com.sap.cds.services.mt.SubscribeEventContext;
import com.sap.cds.services.mt.UnsubscribeEventContext;
import com.sap.cds.services.mt.UpgradeEventContext;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cds.services.utils.OrderConstants;
import com.sap.cds.services.utils.StringUtils;
import com.sap.cds.services.utils.model.DynamicModelUtils;
import com.sap.cloud.mt.subscription.HanaEncryptionTool;
import com.sap.cloud.mt.subscription.HdiContainerManager;
import com.sap.cloud.mt.subscription.InstanceLifecycleManager;
import com.sap.cloud.mt.subscription.LiquibaseParameters;
import com.sap.cloud.mt.subscription.PollingParameters;
import com.sap.cloud.mt.subscription.ProvisioningService;
import com.sap.cloud.mt.subscription.SecurityChecker;
import com.sap.cloud.mt.subscription.ServiceSpecification;
import com.sap.cloud.mt.subscription.SidecarAccess;
import com.sap.cloud.mt.subscription.Subscriber;
import com.sap.cloud.mt.subscription.SubscriberBuilder;
import com.sap.cloud.mt.subscription.SubscriberImpl;
import com.sap.cloud.mt.subscription.exceptions.AuthorityError;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.NotFound;
import com.sap.cloud.mt.subscription.exceptions.NotSupported;
import com.sap.cloud.mt.subscription.exceptions.ParameterError;
import com.sap.cloud.mt.subscription.exits.Exits;
import com.sap.cloud.mt.subscription.exits.SubscribeExit;
import com.sap.cloud.mt.subscription.exits.UnSubscribeExit;
import com.sap.cloud.mt.subscription.json.DeletePayload;
import com.sap.cloud.mt.subscription.json.SubscriptionPayload;
import com.sap.cloud.mt.tools.api.RequestEnhancer;
import com.sap.cloud.mt.tools.api.ResilienceConfig;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultDestinationLoader;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.security.BasicCredentials;
import com.sap.xsa.core.instancemanager.client.InstanceCreationOptions;
/**
* The default handler for subscription events.
*/
@ServiceName(DeploymentService.DEFAULT_NAME)
public class MtDeploymentServiceHandler implements EventHandler {
private static final Logger logger = LoggerFactory.getLogger(MtDeploymentServiceHandler.class);
protected static final ObjectMapper mapper = new ObjectMapper();
/**
* The subscriber that performs the actual tenant onboarding
*/
protected final Subscriber subscriber;
/**
* Thread local used to pass the instance manager options to the mt lib
*/
protected final ThreadLocal options = new ThreadLocal<>();
public MtDeploymentServiceHandler(InstanceLifecycleManager instanceLifecycleManager, CdsRuntime runtime) {
try {
MultiTenancy config = runtime.getEnvironment().getCdsProperties().getMultiTenancy();
MtUtils mtUtils = new MtUtils(runtime);
DynamicModelUtils dynamicModelUtils = new DynamicModelUtils(runtime);
RequestEnhancer authenticationEnhancer = dynamicModelUtils.getAuthenticationEnhancer();
ResilienceConfig resilienceConfig = dynamicModelUtils.getResilienceConfig();
List destinations = new ArrayList<>();
HdiContainerManager hdiContainerManager = null;
if (!StringUtils.isEmpty(config.getDeployer().getUrl())) {
PollingParameters pollingParameters = PollingParameters.Builder.create()
.interval(Duration.ofSeconds(5))
.timeout(config.getDeployer().getAsyncTimeout())
.build();
hdiContainerManager = new HdiContainerManager(ServiceSpecification.Builder.create()
.polling(pollingParameters)
.resilienceConfig(resilienceConfig)
.build(), null);
destinations.add(DefaultHttpDestination.builder((config.getDeployer().getUrl()))
.name(HdiContainerManager.HDI_DEPLOYER_DESTINATION)
.basicCredentials(new BasicCredentials(config.getDeployer().getUser(), config.getDeployer().getPassword()))
.build());
}
SidecarAccess sidecar = null;
if (!StringUtils.isEmpty(config.getSidecar().getUrl())) {
PollingParameters pollingParameters = PollingParameters.Builder.create()
.interval(config.getSidecar().getPollingInterval())
.timeout(config.getSidecar().getPollingTimeout())
.build();
sidecar = new SidecarAccess(ServiceSpecification.Builder.create()
.requestEnhancer(authenticationEnhancer)
.resilienceConfig(resilienceConfig)
.polling(pollingParameters)
.build());
destinations.add(DefaultHttpDestination.builder(config.getSidecar().getUrl())
.name(SidecarAccess.MTX_SIDECAR_DESTINATION)
.build());
}
ProvisioningService provisioningService = null;
if (config.getMtxs().isEnabled()) {
String url = mtUtils.getProvisioningServiceUrl();
if (StringUtils.isEmpty(url)) {
throw new ErrorStatusException(CdsErrorStatuses.NO_PROVISIONINGSERVICE_URL);
}
PollingParameters pollingParameters = PollingParameters.Builder.create()
.interval(config.getProvisioning().getPollingInterval())
.timeout(config.getProvisioning().getPollingTimeout())
.build();
provisioningService = new ProvisioningService(ServiceSpecification.Builder.create()
.requestEnhancer(authenticationEnhancer)
.resilienceConfig(resilienceConfig)
.polling(pollingParameters)
.build());
destinations.add(DefaultHttpDestination.builder(mtUtils.getProvisioningServiceUrl())
.name(ProvisioningService.MTX_PROVISIONING_SERVICE_DESTINATION)
.build());
}
// TODO sync with other destinations
DefaultDestinationLoader destinationLoader = new DefaultDestinationLoader();
destinations.forEach(d -> destinationLoader.registerDestination(d));
DestinationAccessor.prependDestinationLoader(destinationLoader);
String encryptionMode = config.getDataSource().getHanaEncryptionMode();
subscriber = SubscriberBuilder.create()
.instanceLifecycleManager(instanceLifecycleManager)
.hdiContainerManager(hdiContainerManager)
.sidecar(sidecar)
.provisioningService(provisioningService)
.exits(getExits())
.securityChecker(getSecurityChecker())
.hanaEncryptionMode(encryptionMode != null ? HanaEncryptionTool.DbEncryptionMode.valueOf(encryptionMode) : null)
.liquibaseParameters(new LiquibaseParameters(config.getLiquibase().getChangeLog(), config.getLiquibase().getContexts(), null))
.baseUiUrl(config.getAppUi().getUrl())
.urlSeparator(config.getAppUi().getTenantSeparator())
.build();
} catch (IllegalArgumentException | InternalError e) {
throw new ErrorStatusException(CdsErrorStatuses.SUBSCRIBER_FAILED, e);
}
}
private SecurityChecker getSecurityChecker() {
return new SecurityChecker() {
@Override
public void checkSubscriptionAuthority() throws AuthorityError {
// done in before handler
}
@Override
public void checkInitDbAuthority() throws AuthorityError {
// done in before handler
}
};
}
private Exits getExits() {
return new Exits(new UnSubscribeExit() {
@Override
public Boolean onBeforeUnsubscribe(String tenantId, DeletePayload deletePayload) {
return true;
}
@Override
public Boolean onBeforeAsyncUnsubscribe(String tenantId, DeletePayload deletePayload) {
return true;
}
}, new SubscribeExit() {
@Override
public InstanceCreationOptions onBeforeSubscribe(String tenantId, SubscriptionPayload subscriptionPayload)
throws InternalError {
return options.get();
}
@Override
public InstanceCreationOptions onBeforeAsyncSubscribe(String tenantId, SubscriptionPayload subscriptionPayload) throws InternalError {
return options.get();
}
}, null, null, null, false);
}
@On
@HandlerOrder(OrderConstants.On.PRIORITY)
protected void onSubscribe(SubscribeEventContext context) {
try {
options.set(buildInstanceCreationOptions(context));
var payload = new SubscriptionPayload(context.getOptions());
subscriber.subscribe(context.getTenant(), payload, forwardToken(context));
} catch (InternalError e) {
throw new ErrorStatusException(CdsErrorStatuses.SUBSCRIPTION_FAILED, context.getTenant(), e) ;
} catch (ParameterError e) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST, e);
} catch (AuthorityError e) {
throw new ErrorStatusException(ErrorStatuses.FORBIDDEN, e);
} finally {
options.remove();
}
}
@On
protected void onUnsubscribe(UnsubscribeEventContext context) {
try {
logger.info("Deleting subscription of tenant '{}'", context.getTenant());
var payload = new DeletePayload(context.getOptions());
subscriber.unsubscribe(context.getTenant(), payload, forwardToken(context));
} catch (InternalError e) {
throw new ErrorStatusException(CdsErrorStatuses.UNSUBSCRIPTION_FAILED, context.getTenant(), e) ;
} catch (ParameterError e) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST, e);
} catch (AuthorityError e) {
throw new ErrorStatusException(ErrorStatuses.FORBIDDEN, e);
}
}
@On
@HandlerOrder(OrderConstants.On.PRIORITY)
protected void onUpgrade(UpgradeEventContext context) {
try {
if (subscriber instanceof SubscriberImpl) {
// HDI deployer
subscriber.setupDbTables(context.getTenants());
} else {
// Sidecar
new AsyncSidecarUpgradeHelper(subscriber).deploy(context.getTenants());
}
} catch (NotFound | InternalError e) {
throw new ErrorStatusException(CdsErrorStatuses.DEPLOYMENT_FAILED, String.join(",", context.getTenants()), e);
} catch (NotSupported e) {
throw new ErrorStatusException(ErrorStatuses.NOT_IMPLEMENTED, e);
} catch (ParameterError e) {
throw new ErrorStatusException(ErrorStatuses.BAD_REQUEST, e);
} catch (AuthorityError e) {
throw new ErrorStatusException(ErrorStatuses.FORBIDDEN, e);
}
}
// only required for subscribe / unsubscribe in old MTX Sidecar, as client credentials token doesn't contain proper scopes, as they are solely granted to CIS
protected String forwardToken(EventContext context) {
AuthenticationInfo authenticationInfo = context.getAuthenticationInfo();
if(authenticationInfo != null && authenticationInfo.is(JwtTokenAuthenticationInfo.class)) {
JwtTokenAuthenticationInfo accessToken = authenticationInfo.as(JwtTokenAuthenticationInfo.class);
String jwt = accessToken.getToken();
if (jwt == null || jwt.startsWith("Bearer ")) {
return jwt;
}
return "Bearer " + jwt;
} else {
return null;
}
}
@SuppressWarnings("unchecked")
protected InstanceCreationOptions buildInstanceCreationOptions(SubscribeEventContext context) {
InstanceCreationOptions ico = new InstanceCreationOptions();
Object provisioning = context.getOptions().get("provisioningParameters");
if (provisioning instanceof Map) {
ico.withProvisioningParameters((Map) provisioning);
}
Object binding = context.getOptions().get("bindingParameters");
if (binding instanceof Map) {
ico.withBindingParameters((Map) binding);
}
return ico.getProvisioningParameters() != null || ico.getBindingParameters() != null ? ico : null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy