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.1
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.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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy