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
/*******************************************************************************
* © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved.
******************************************************************************/
package com.sap.cloud.mt.subscription;
import com.sap.cloud.mt.subscription.HanaEncryptionTool.DbEncryptionMode;
import com.sap.cloud.mt.subscription.InstanceLifecycleManager.ContainerStatus;
import com.sap.cloud.mt.subscription.exceptions.AuthorityError;
import com.sap.cloud.mt.subscription.exceptions.InternalError;
import com.sap.cloud.mt.subscription.exceptions.NotSupported;
import com.sap.cloud.mt.subscription.exceptions.ParameterError;
import com.sap.cloud.mt.subscription.exceptions.UnknownTenant;
import com.sap.cloud.mt.subscription.exits.AfterSubscribeMethod;
import com.sap.cloud.mt.subscription.exits.AfterUnSubscribeMethod;
import com.sap.cloud.mt.subscription.exits.Exits;
import com.sap.cloud.mt.subscription.json.ApplicationDependency;
import com.sap.cloud.mt.subscription.json.Cloner;
import com.sap.cloud.mt.subscription.json.DeletePayload;
import com.sap.cloud.mt.subscription.json.SubscriptionPayload;
import com.sap.cloud.mt.tools.api.ResilienceConfig;
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;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class Subscriber provides methods to subscribe new tenants, start DB artifact deployments and unsubscribe tenants.
*/
public class SubscriberImpl implements Subscriber {
public static final String ALL_TENANTS = "all";
private static final Logger logger = LoggerFactory.getLogger(SubscriberImpl.class);
private final InstanceLifecycleManager instanceLifecycleManager;
private final DbDeployer dbDeployer;
private final Exits exits;
private final String baseUiUrl;
private final String urlSeparator;
private final SecurityChecker securityChecker;
private final SaasRegistry saasRegistry;
private final boolean withoutAuthorityCheck;
private final DbEncryptionMode hanaEncryptionMode;
private final ResilienceConfig resilienceConfig;
SubscriberImpl(InstanceLifecycleManager instanceLifecycleManager, DbDeployer dbDeployer,
String baseUiUrl,
String urlSeparator,
Exits exits,
SecurityChecker securityChecker,
SaasRegistry saasRegistry,
boolean withoutAuthorityCheck,
DbEncryptionMode hanaEncryptionMode,
ResilienceConfig resilienceConfig) throws InternalError {
this.resilienceConfig = resilienceConfig != null ? resilienceConfig : ResilienceConfig.NONE;
this.instanceLifecycleManager = instanceLifecycleManager;
this.dbDeployer = dbDeployer;
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;
this.withoutAuthorityCheck = withoutAuthorityCheck;
this.hanaEncryptionMode = hanaEncryptionMode;
}
@Override
public void unsubscribe(String tenantId, DeletePayload deletePayload) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
Tools.checkExternalTenantId(tenantId);
boolean processUnsubscribe = exits.getBeforeUnSubscribeMethod().call(tenantId, Cloner.clone(deletePayload));
//The exit has to return true, otherwise nothing is deleted
if (processUnsubscribe) {
deleteInstance(tenantId, deletePayload, exits.getAfterUnSubscribeMethod());
} else {
logger.debug("Unsubscribe exit returned false=> No unsubscription performed");
}
}
@Override
public List getApplicationDependencies() throws AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
return exits.getDependencyExit().onGetDependencies();
}
@Override
public String subscribe(String tenantId, SubscriptionPayload subscriptionPayload) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
Tools.checkExternalTenantId(tenantId);
ServiceCreateOptions serviceCreateOptions = null;
try {
serviceCreateOptions = new ServiceCreateOptions(exits.getBeforeSubscribeMethod().call(tenantId, Cloner.clone(subscriptionPayload)));
} catch (InternalError internalError) {
exits.getAfterSubscribeMethod().call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
String subscriptionUrl = Tools.getApplicationUrl(subscriptionPayload, exits.getSubscribeExit()::uiURL, exits.getSubscribeExit()::uiURL, baseUiUrl, urlSeparator);
var payloadAccess = SubscriptionPayloadAccess.create(subscriptionPayload.getMap());
HanaEncryptionTool.addEncryptionParameter(serviceCreateOptions, hanaEncryptionMode, payloadAccess);
createInstanceAndOnBoard(tenantId, subscriptionPayload, subscriptionUrl, serviceCreateOptions, exits.getAfterSubscribeMethod(), resilienceConfig);
return subscriptionUrl;
}
@Override
public String getSubscribeUrl(SubscriptionPayload subscriptionPayload) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkSubscriptionAuthority();
}
return Tools.getApplicationUrl(subscriptionPayload, exits.getSubscribeExit()::uiURL, exits.getSubscribeExit()::uiURL, baseUiUrl, urlSeparator);
}
@Override
public void setupDbTables(List tenants) throws InternalError, ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkInitDbAuthority();
}
setupDbTablesInt(tenants);
}
@Override
public String setupDbTablesAsync(List tenants) throws ParameterError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkInitDbAuthority();
}
for (String tenantId : tenants) {
Tools.checkExternalTenantId(tenantId);
}
ExecutorService executor = null;
try {
executor = Executors.newSingleThreadExecutor();
CompletableFuture.supplyAsync(() -> {
try {
setupDbTablesInt(tenants);
} catch (InternalError internalError) {
logger.error("Could not init DB asynchronously. Error is {}", internalError.getMessage());
} catch (ParameterError | AuthorityError parameterError) {
//cannot happen, tenant Id already checked
}
return "";
}
, executor);
} finally {
if (executor != null) executor.shutdown();
}
return "";
}
@Override
public String updateStatus(String jobId) throws NotSupported, InternalError, AuthorityError {
if (!withoutAuthorityCheck) {
securityChecker.checkInitDbAuthority();
}
logger.debug("Update status is only supported with sidecar");
throw new NotSupported("Update status is only supported with sidecar");
}
@Override
public void callSaasRegistry(boolean ok, String message, String applicationUrl, String saasRegistryUrl) throws InternalError {
saasRegistry.callBackSaasRegistry(ok, message, applicationUrl, saasRegistryUrl);
}
@Override
public void checkAuthority(SecurityChecker.Authority authority) throws AuthorityError {
securityChecker.checkAuthority(authority);
}
private void setupDbTablesInt(List tenants) throws InternalError, ParameterError, AuthorityError {
for (String tenantId : tenants) {
Tools.checkExternalTenantId(tenantId);
}
if (tenants.size() == 1 && tenants.get(0).equals(ALL_TENANTS)) {
setupDbTables(new ArrayList<>(instanceLifecycleManager.getAllTenants(true)));
return;
}
if (exits.getInitDbExit() != null) exits.getInitDbExit().onBeforeInitDb(tenants);
final String[] message = {""};
tenants.stream()
.filter(FilterTenants.realTenants())
.forEach(tenantId -> {
try {
DataSourceInfo dataSourceAndInfo = instanceLifecycleManager.getDataSourceInfo(tenantId, false);
dbDeployer.populate(dataSourceAndInfo, tenantId);
} catch (InternalError e) {
if (message[0].isEmpty()) {
message[0] = "Error in deployment:";
}
message[0] += "\n Could not perform deployment for tenant " + tenantId + " Error is:" + e.getMessage();
} 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 {
instanceLifecycleManager.deleteInstance(tenantId);
afterUnSubscribeMethod.call(tenantId, Cloner.clone(deletePayload));
}
private String createInstanceAndOnBoard(String tenantId, SubscriptionPayload subscriptionPayload,
String url, ServiceCreateOptions serviceCreateOptions,
AfterSubscribeMethod exit, ResilienceConfig resilienceConfig) throws InternalError {
final AtomicReference instanceStatusRef = new AtomicReference<>(null);
resilienceConfig.tryWhile(() -> {
try {
instanceStatusRef.set(instanceLifecycleManager.getContainerStatus(tenantId));
return (instanceStatusRef.get() == ContainerStatus.CREATION_IN_PROGRESS)
|| (instanceStatusRef.get() == ContainerStatus.OK && !instanceLifecycleManager.hasCredentials(tenantId, false));
} catch (InternalError e) {
logger.error("Could not get status for tenant %s".formatted(tenantId), e);
return true;
}
});
var instanceStatus = instanceStatusRef.get();
if (instanceStatus == ContainerStatus.CREATION_ERROR) {
logger.debug("Container for tenant {} has status CREATION_FAILED", tenantId);
logger.debug("Delete container to fix problem");
instanceLifecycleManager.deleteInstance(tenantId);
instanceStatus = instanceLifecycleManager.getContainerStatus(tenantId);
}
logger.debug("Subscribe tenant {}", tenantId);
if (instanceStatus == ContainerStatus.OK) {
deploy(tenantId, subscriptionPayload, exit);
return url;
} else if (instanceStatus == ContainerStatus.DOES_NOT_EXIST) {
logger.debug("Create new instance for tenant {}", tenantId);
var provisioningParameters = new ProvisioningParameters(serviceCreateOptions.getProvisioningParameters());
var bindingParameters = new BindingParameters(serviceCreateOptions.getBindingParameters());
try {
instanceLifecycleManager.createNewInstance(tenantId, provisioningParameters, bindingParameters);
} catch (InternalError internalError) {
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
deploy(tenantId, subscriptionPayload, exit);
return url;
} else {
logger.error("Instance for tenant id {} has wrong status {}", tenantId, instanceStatus);
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw new InternalError("Instance has wrong status");
}
}
private void deploy(String tenantId, SubscriptionPayload subscriptionPayload, AfterSubscribeMethod exit) throws InternalError {
try {
DataSourceInfo dataSourceInfo = instanceLifecycleManager.getDataSourceInfo(tenantId, false);
logger.debug("Deploy to DB container for tenant {}", tenantId);
dbDeployer.populate(dataSourceInfo, tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), true);
} 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 DB container for tenant {}", tenantId);
exit.call(tenantId, Cloner.clone(subscriptionPayload), false);
throw internalError;
}
}
public DbDeployer getDbDeployer() {
return dbDeployer;
}
}