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

com.sap.cloud.mt.subscription.SubscriberImpl Maven / Gradle / Ivy

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