All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.services.mt.impl.MtDeploymentServiceHandler Maven / Gradle / Ivy

There is a newer version: 3.6.0
Show newest version
/**************************************************************************
 * (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