com.sap.cloud.mt.subscription.SubscriberImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of multi-tenant-subscription Show documentation
Show all versions of multi-tenant-subscription Show documentation
Spring Boot Enablement Parent
/******************************************************************************
* © 2020 SAP SE or an SAP affiliate company. All rights reserved. *
******************************************************************************/
package com.sap.cloud.mt.subscription;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.*;
import com.sap.cloud.mt.subscription.exits.*;
import com.sap.cloud.mt.subscription.json.*;
import com.sap.xsa.core.instancemanager.client.InstanceCreationOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Class Subscriber provides methods to subscribe new tenants, start hdi deployments and unsubscribe tenants.
* It works together with the Hana instance manager, that provides a rest API to create and delete hdi containers for tenants, and the dynamic
* HDI deployment app, a node.js application that contains the database artifacts definitions and deploys them into an hdi container if called via its
* rest API.
*/
public class SubscriberImpl extends AbstractSubscriber {
public static final String ALL_TENANTS = "all";
private static final Logger logger = LoggerFactory.getLogger(SubscriberImpl.class);
private final InstanceLifecycleManager instanceLifecycleManager;
private final HdiContainerManager hdiContainerManager;
private final Exits exits;
private final String baseUiUrl;
private final String urlSeparator;
private final SecurityChecker securityChecker;
private final SaasRegistry saasRegistry;
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
SubscriberImpl(InstanceLifecycleManager instanceLifecycleManager, HdiContainerManager hdiContainerManager,
String baseUiUrl,
String urlSeparator,
Exits exits,
SecurityChecker securityChecker,
SaasRegistry saasRegistry) throws InternalError {
this.instanceLifecycleManager = instanceLifecycleManager;
this.hdiContainerManager = hdiContainerManager;
this.exits = exits;
this.baseUiUrl = baseUiUrl;
this.urlSeparator = urlSeparator;
if (exits.getUnSubscribeExit() == null) throw new InternalError("No unsubscribe exit found");
this.securityChecker = securityChecker;
this.saasRegistry = saasRegistry;
}
@Override
public void unsubscribe(String tenantId, DeletePayload deletePayload, String jwt) throws InternalError, UnknownTenant, ParameterError, AuthorityError {
unsubscribe(tenantId, deletePayload, jwt, null, null);
}
@Override
public void unsubscribe(String tenantId, DeletePayload deletePayload, String jwt, String saasRegistryUrl, String asyncUnSubscribeCallBackUrl) throws InternalError, UnknownTenant, ParameterError, AuthorityError {
boolean asyncCall = isAsyncCall(saasRegistryUrl);
securityChecker.checkSubscriptionAuthority();
Tools.checkTenantId(tenantId);
BeforeUnSubscribeMethod beforeUnSubscribeMethod;
AfterUnSubscribeMethod afterUnSubscribeMethod;
if (asyncCall) {
beforeUnSubscribeMethod = exits.getUnSubscribeExit()::onBeforeAsyncUnsubscribe;
afterUnSubscribeMethod = exits.getUnSubscribeExit()::onAfterAsyncUnsubscribe;
} else {
beforeUnSubscribeMethod = exits.getUnSubscribeExit()::onBeforeUnsubscribe;
afterUnSubscribeMethod = exits.getUnSubscribeExit()::onAfterUnsubscribe;
}
boolean processUnsubscribe = beforeUnSubscribeMethod.call(tenantId, Cloner.clone(deletePayload));
//The exit has to return true, otherwise nothing is deleted
if (processUnsubscribe) {
if (asyncCall) {
unsubscribeAsynchronously(tenantId, saasRegistryUrl, deletePayload, afterUnSubscribeMethod);
} else {
deleteInstance(tenantId, deletePayload, afterUnSubscribeMethod);
}
} else {
logger.debug("Unsubscribe exit returned false=> No unsubscription performed");
throw new InternalError("Unsubscribe exit returned false=> No unsubscription performed");
}
}
private boolean isAsyncCall(String saasRegistryUrl) {
return saasRegistryUrl != null && !saasRegistryUrl.isEmpty();
}
private void unsubscribeAsynchronously(String tenantId, String saasRegistryUrl, DeletePayload deletePayload,
AfterUnSubscribeMethod afterUnSubscribeMethod) throws InternalError {
if (saasRegistry == null) {
logger.error("No saas registry service instance bound to application");
throw new InternalError("No saas registry service instance bound to application");
}
CompletableFuture onBoard = CompletableFuture.runAsync(() -> {
long startTime = System.nanoTime();
try {
deleteInstance(tenantId, deletePayload, afterUnSubscribeMethod);
waitSomeTime(startTime);
saasRegistry.callBackSaasRegistry(true, null, null, saasRegistryUrl);
} catch (InternalError e) {
logger.error(e.getMessage());
waitSomeTime(startTime);
try {
saasRegistry.callBackSaasRegistry(false, e.getMessage(), null, saasRegistryUrl);
} catch (InternalError internalError) {
logger.error(internalError.getMessage());
}
} catch (UnknownTenant e) {
//tenant seems to be unsubscribed in parallel task
logger.debug(e.getMessage());
waitSomeTime(startTime);
try {
saasRegistry.callBackSaasRegistry(true, null, null, saasRegistryUrl);
} catch (InternalError internalError) {
logger.error(internalError.getMessage());
}
}
}, executorService);
}
@Override
public List getApplicationDependencies(String jwt) throws AuthorityError {
securityChecker.checkSubscriptionAuthority();
return exits.getDependencyExit().onGetDependencies();
}
@Override
public String subscribe(String tenantId, SubscriptionPayload subscriptionPayload, String jwt) throws InternalError, ParameterError, AuthorityError {
return subscribe(tenantId, subscriptionPayload, jwt, null, null);
}
@Override
public String subscribe(String tenantId, SubscriptionPayload subscriptionPayload, String jwt, String saasRegistryUrl,
String asyncSubscribeCallBackUrl) throws InternalError, ParameterError, AuthorityError {
boolean asyncCall = isAsyncCall(saasRegistryUrl);
securityChecker.checkSubscriptionAuthority();
Tools.checkTenantId(tenantId);
BeforeSubscribeMethod beforeExit;
AfterSubscribeMethod afterExit;
if (asyncCall) {
beforeExit = exits.getSubscribeExit()::onBeforeAsyncSubscribe;
afterExit = exits.getSubscribeExit()::onAfterAsyncSubscribe;
} else {
beforeExit = exits.getSubscribeExit()::onBeforeSubscribe;
afterExit = exits.getSubscribeExit()::onAfterSubscribe;
}
InstanceCreationOptions instanceCreationOptions = null;
try {
instanceCreationOptions = beforeExit.call(tenantId, Cloner.clone(subscriptionPayload));
} catch (InternalError internalError) {
logger.error(internalError.getMessage());
afterExit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
//get URL from exit
String subscriptionUrl = (exits.getSubscribeExit().uiURL() != null) ? exits.getSubscribeExit().uiURL().toExternalForm() : null;
//get URL from external parameters
if (subscriptionPayload == null || subscriptionPayload.subscribedSubdomain == null) {
if (subscriptionPayload == null) throw new ParameterError("No subscription payload available");
else throw new ParameterError("No sub domain passed in subscription payload");
}
if (subscriptionUrl == null) subscriptionUrl = UiUrlCreator.createUrl(subscriptionPayload.subscribedSubdomain, baseUiUrl, urlSeparator);
if (asyncCall) {
subscribeAsynchronously(tenantId, jwt, saasRegistryUrl, subscriptionPayload, afterExit, instanceCreationOptions, subscriptionUrl);
} else {
createInstanceAndOnBoard(tenantId, subscriptionPayload, jwt, subscriptionUrl, instanceCreationOptions, afterExit);
}
return subscriptionUrl;
}
private void subscribeAsynchronously(String tenantId, String jwt, String saasRegistryUrl, SubscriptionPayload subscriptionPayload,
AfterSubscribeMethod afterExit, InstanceCreationOptions instanceCreationOptions, String subscriptionUrl) throws InternalError {
if (saasRegistry == null) {
logger.error("No saas registry service instance bound to application");
throw new InternalError("No saas registry service instance bound to application");
}
final String finalSubscriptionUrl = subscriptionUrl;
final InstanceCreationOptions finalInstanceCreationOptions = Cloner.clone(instanceCreationOptions);
CompletableFuture onBoard = CompletableFuture.runAsync(() -> {
long startTime = System.nanoTime();
try {
createInstanceAndOnBoard(tenantId, subscriptionPayload, jwt, finalSubscriptionUrl, finalInstanceCreationOptions, afterExit);
waitSomeTime(startTime);
saasRegistry.callBackSaasRegistry(true, null, finalSubscriptionUrl, saasRegistryUrl);
} catch (InternalError | ParameterError | AuthorityError e) {
logger.error(e.getMessage());
waitSomeTime(startTime);
try {
saasRegistry.callBackSaasRegistry(false, e.getMessage(), null, saasRegistryUrl);
} catch (InternalError internalError) {
logger.error(e.getMessage());
}
}
}, executorService);
}
@Override
public void setupDbTables(List tenants, String jwt) throws InternalError, ParameterError, AuthorityError {
securityChecker.checkInitDbAuthority();
setupDbTablesInt(tenants, jwt);
}
@Override
public String setupDbTablesAsync(List tenants, String jwt) throws ParameterError, AuthorityError {
securityChecker.checkInitDbAuthority();
for (String tenantId : tenants) {
Tools.checkTenantId(tenantId);
}
ExecutorService executor = null;
try {
executor = Executors.newSingleThreadExecutor();
CompletableFuture.supplyAsync(() -> {
try {
setupDbTablesInt(tenants, jwt);
} catch (InternalError internalError) {
logger.error("Could not init DB asynchronously. Error is {}", internalError.getMessage());
} catch (ParameterError parameterError) {
//cannot happen, tenant Id already checked
} catch (AuthorityError authorityError) {
//already checked
}
return "";
}
, executor);
} finally {
if (executor != null) executor.shutdown();
}
return "";
}
@Override
public String updateStatus(String jobId, String jwt) throws NotSupported, InternalError, AuthorityError {
securityChecker.checkInitDbAuthority();
logger.debug("Update status is only supported with sidecar");
throw new NotSupported("Update status is only supported with sidecar");
}
private void setupDbTablesInt(List tenants, String jwt) throws InternalError, ParameterError, AuthorityError {
for (String tenantId : tenants) {
Tools.checkTenantId(tenantId);
}
if (tenants.size() == 1 && tenants.get(0).equals(ALL_TENANTS)) {
setupDbTables(new ArrayList<>(instanceLifecycleManager.getAllTenants()), jwt);
return;
}
if (exits.getInitDbExit() != null) exits.getInitDbExit().onBeforeInitDb(tenants);
final String[] message = {""};
tenants.stream()
.filter(tenantId -> !Tools.isTechnicalContainer(tenantId))
.forEach(tenantId -> {
try {
DataSourceInfo dataSourceAndInfo = instanceLifecycleManager.getDataSourceInfo(tenantId, false);
hdiContainerManager.populateHdiContainer(dataSourceAndInfo, tenantId);
} catch (InternalError e) {
if (message[0].isEmpty()) {
message[0] = "Error in deployment:";
}
message[0] += "\n Could not perform deployment for tenant " + tenantId;
} catch (UnknownTenant unknownTenant) {
// ignore, seems to be deleted in the meantime
}
});
if (exits.getInitDbExit() != null) exits.getInitDbExit().onAfterInitDb(message[0].isEmpty());
if (!message[0].isEmpty()) throw new InternalError(message[0]);
}
private void deleteInstance(String tenantId, DeletePayload deletePayload, AfterUnSubscribeMethod afterUnSubscribeMethod)
throws InternalError, UnknownTenant {
instanceLifecycleManager.deleteInstance(tenantId);
afterUnSubscribeMethod.call(tenantId, Cloner.clone(deletePayload));
}
public String createInstanceAndOnBoard(String tenantId, SubscriptionPayload subscriptionPayload, String jwt,
String url, InstanceCreationOptions instanceCreationOptions,
AfterSubscribeMethod exit) throws InternalError, ParameterError, AuthorityError {
InstanceLifecycleManager.ContainerStatus instanceStatus = instanceLifecycleManager.getContainerStatus(tenantId);
if (instanceStatus == InstanceLifecycleManager.ContainerStatus.CREATION_ERROR) {
try {
logger.debug("Container for tenant {} has status CREATION_FAILED", tenantId);
logger.debug("Delete container to fix problem");
instanceLifecycleManager.deleteInstance(tenantId);
instanceStatus = instanceLifecycleManager.getContainerStatus(tenantId);
} catch (UnknownTenant unknownTenant) {
//ignore it
}
}
logger.debug("Subscribe tenant " + tenantId);
if (instanceStatus == InstanceLifecycleManager.ContainerStatus.OK) {
try {
DataSourceInfo dataSourceInfo = instanceLifecycleManager.getDataSourceInfo(tenantId, false);
hdiContainerManager.populateHdiContainer(dataSourceInfo, tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), true);
return url;
} catch (UnknownTenant unknownTenant) {
// is actually impossible as the status was ok and cache is used
logger.error("Tenant {} was deleted in parallel session", tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw new InternalError("Tenant was deleted in parallel session");
} catch (InternalError internalError) {
logger.error("Could not deploy to hdi container for tenant " + tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
} else if (instanceStatus == InstanceLifecycleManager.ContainerStatus.DOES_NOT_EXIST) {
logger.debug("Create new instance for tenant " + tenantId);
instanceLifecycleManager.createNewInstance(tenantId, instanceCreationOptions);
DataSourceInfo dataSourceInfo = null;
try {
dataSourceInfo = instanceLifecycleManager.getDataSourceInfo(tenantId, false);
} catch (UnknownTenant unknownTenant) {
logger.error("Tenant {} was deleted in parallel session", tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw new InternalError("Could not create hdi container");
}
logger.debug("Populate schema for new for tenant " + tenantId);
try {
hdiContainerManager.populateHdiContainer(dataSourceInfo, tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), true);
return url;
} catch (InternalError internalError) {
logger.error("Could not deploy to hdi container for tenant " + tenantId);
try {
logger.debug("Delete hdi container for tenant " + tenantId);
instanceLifecycleManager.deleteInstance(tenantId);
} catch (Exception e) {
logger.error("Could not delete hdi container for tenant " + tenantId);
}
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
} else {
logger.error("Instance for tenant id {} has wrong status {}", tenantId, instanceStatus.toString());
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw new InternalError("Instance has wrong status");
}
}
// Asynchronous subscribe without sidecar is realized by the lib with starting a parallel task. The parallel tasks calls the
// dynamic HDI deployment program synchronously. Therefore no callback is needed.
@Override
public void unsubscribeCallback(SidecarUnSubscribeCallBackPayload sidecarCallBackPayload, String jwt) throws InternalError, AuthorityError {
throw new InternalError("Callback not supported without mtx/sidecar");
}
// Asynchronous unsubscribe without sidecar is realized by the lib with starting a parallel task. The parallel tasks calls the
// dynamic HDI deployment program synchronously. Therefore no callback is needed.
@Override
public void subscribeCallback(String jwt, SidecarSubscribeCallBackPayload sidecarCallBackPayload) throws InternalError, AuthorityError {
throw new InternalError("Callback not supported without mtx/sidecar");
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy